snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
37
packages/engine-formula/README.md
Normal file
37
packages/engine-formula/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# @univerjs/engine-formula
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/engine-formula)
|
||||
[](https://npmjs.com/package/@univerjs/engine-formula)
|
||||
[](https://npmjs.com/package/@univerjs/engine-formula)
|
||||
|
||||
`@univerjs/engine-formula` is Univer's formula runtime. It parses formulas, registers functions and descriptions, manages dependencies, calculates results, and exposes shared formula editor helpers.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/engine-formula` | `UniverEngineFormula` | No | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/engine-formula
|
||||
# or
|
||||
npm install @univerjs/engine-formula
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
|
||||
univer.registerPlugin(UniverFormulaEnginePlugin);
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/engine-formula)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
98
packages/engine-formula/package.json
Normal file
98
packages/engine-formula/package.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "@univerjs/engine-formula",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Formula parsing, function registration, dependency management, and calculation engine for Univer.",
|
||||
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"univer",
|
||||
"formula",
|
||||
"formula-engine",
|
||||
"calculation",
|
||||
"spreadsheet"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./facade": "./src/facade/index.ts",
|
||||
"./locale/*": "./src/locale/*.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./locale/*": {
|
||||
"import": "./lib/es/locale/*.js",
|
||||
"require": "./lib/cjs/locale/*.js",
|
||||
"types": "./lib/types/locale/*.d.ts"
|
||||
},
|
||||
"./facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@flatten-js/interval-tree": "1.1.3",
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/rpc": "workspace:*",
|
||||
"decimal.js": "^10.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"rxjs": "^7.8.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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 { FunctionType, matchToken, sequenceNodeType } from '@univerjs/engine-formula';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
findFormulaStructuredReferences,
|
||||
getFormulaHighlightDataStream,
|
||||
getFormulaReplaceResult,
|
||||
resolveFormulaReferenceEditingContext,
|
||||
} from '../formula-editor-helpers';
|
||||
|
||||
describe('formula editor helpers', () => {
|
||||
it('replaces a partial function and appends its opening bracket', () => {
|
||||
expect(getFormulaReplaceResult(['SU'], 0, 'SUM', FunctionType.Math)).toEqual({
|
||||
text: `SUM${matchToken.OPEN_BRACKET}`,
|
||||
offset: -2,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not append a bracket for table references', () => {
|
||||
expect(getFormulaReplaceResult(['SalesTa'], 0, 'SalesTable', FunctionType.Table)).toEqual({
|
||||
text: 'SalesTable',
|
||||
offset: -3,
|
||||
});
|
||||
});
|
||||
|
||||
it('serializes parsed references into an editor document stream', () => {
|
||||
const nodes = [{
|
||||
token: '[Book]Sheet1!A1',
|
||||
nodeType: sequenceNodeType.REFERENCE,
|
||||
startIndex: 0,
|
||||
endIndex: 15,
|
||||
}];
|
||||
|
||||
expect(getFormulaHighlightDataStream('=', nodes)).toBe('=[Book]Sheet1!A1\r\n');
|
||||
});
|
||||
|
||||
it('uses the same cursor rules for adding and replacing references', () => {
|
||||
const reference = {
|
||||
token: 'A1:B2',
|
||||
nodeType: sequenceNodeType.REFERENCE,
|
||||
startIndex: 4,
|
||||
endIndex: 8,
|
||||
};
|
||||
const sequenceNodes = ['SUM(', reference, '+'];
|
||||
|
||||
expect(resolveFormulaReferenceEditingContext({
|
||||
formulaText: 'SUM(A1:B2+',
|
||||
sequenceNodes,
|
||||
offset: 4,
|
||||
})).toMatchObject({ mode: 'add', referenceIndex: -1 });
|
||||
expect(resolveFormulaReferenceEditingContext({
|
||||
formulaText: 'SUM(A1:B2+',
|
||||
sequenceNodes,
|
||||
offset: 9,
|
||||
})).toMatchObject({ mode: 'replace', referenceIndex: 0 });
|
||||
expect(resolveFormulaReferenceEditingContext({
|
||||
formulaText: 'SUM(A1:B2+',
|
||||
sequenceNodes,
|
||||
offset: 10,
|
||||
})).toMatchObject({ mode: 'add', referenceIndex: -1 });
|
||||
});
|
||||
|
||||
it('does not draw a reference in plain formula text', () => {
|
||||
expect(resolveFormulaReferenceEditingContext({
|
||||
formulaText: 'SUM',
|
||||
sequenceNodes: ['SUM'],
|
||||
offset: 3,
|
||||
})).toMatchObject({ mode: 'none', referenceIndex: -1 });
|
||||
});
|
||||
|
||||
it('allows adding a reference when the cursor is before the first sequence node', () => {
|
||||
expect(resolveFormulaReferenceEditingContext({
|
||||
formulaText: 'SUM(A1)',
|
||||
sequenceNodes: [
|
||||
'SUM(',
|
||||
{
|
||||
token: 'A1',
|
||||
nodeType: sequenceNodeType.REFERENCE,
|
||||
startIndex: 4,
|
||||
endIndex: 5,
|
||||
},
|
||||
')',
|
||||
],
|
||||
offset: 0,
|
||||
})).toMatchObject({
|
||||
mode: 'add',
|
||||
nodeIndex: -1,
|
||||
referenceIndex: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it('edits structured table references as selectable references', () => {
|
||||
expect(resolveFormulaReferenceEditingContext({
|
||||
formulaText: 'SUM(SalesTable[Amount])',
|
||||
sequenceNodes: [
|
||||
'SUM(',
|
||||
{
|
||||
token: 'SalesTable[Amount]',
|
||||
nodeType: sequenceNodeType.TABLE,
|
||||
startIndex: 4,
|
||||
endIndex: 21,
|
||||
},
|
||||
')',
|
||||
],
|
||||
offset: 10,
|
||||
})).toMatchObject({ mode: 'replace', referenceIndex: 0 });
|
||||
});
|
||||
|
||||
it('finds cross-unit structured references that are not resolved by the local table map', () => {
|
||||
expect(findFormulaStructuredReferences('SUM([Sales Base]!Orders[Amount])')).toEqual([{
|
||||
token: '[Sales Base]!Orders[Amount]',
|
||||
startIndex: 4,
|
||||
endIndex: 30,
|
||||
}]);
|
||||
});
|
||||
});
|
||||
78
packages/engine-formula/src/basics/__tests__/date.spec.ts
Normal file
78
packages/engine-formula/src/basics/__tests__/date.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 { DateSystem, excelDateSerial, excelDateTimeSerial, excelSerialToDate } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { stripErrorMargin } from '../../engine/utils/math-kit';
|
||||
import { NumberValueObject } from '../../engine/value-object/primitive-object';
|
||||
import {
|
||||
formatDateDefault,
|
||||
getDateSerialNumberByObject,
|
||||
getTwoDateDaysByBasis,
|
||||
isValidDateStr,
|
||||
} from '../date';
|
||||
import { ErrorType } from '../error-type';
|
||||
|
||||
describe('Test date', () => {
|
||||
it('Function excelDateSerial', () => {
|
||||
expect(excelDateSerial(new Date(Date.UTC(1900, 1, 28)))).toBe(59);
|
||||
expect(excelDateSerial(new Date(Date.UTC(1900, 1, 29)))).toBe(61);
|
||||
expect(excelDateSerial(new Date(Date.UTC(1900, 2, 1)))).toBe(61);
|
||||
expect(excelDateSerial(new Date(Date.UTC(1901, 0, 1)))).toBe(367);
|
||||
expect(excelDateSerial(new Date(Date.UTC(2024, 1, 2)))).toBe(45324);
|
||||
});
|
||||
|
||||
it('Function excelDateSerial should account for hours, minutes, and seconds', () => {
|
||||
// Testing midday, which is exactly half of a day
|
||||
expect(excelDateTimeSerial(new Date(Date.UTC(2024, 0, 2, 12, 0, 0)))).toBe(45293.5);
|
||||
// Testing exact hour, which is a quarter of a day
|
||||
expect(excelDateTimeSerial(new Date(Date.UTC(2024, 0, 2, 6, 0, 0)))).toBe(45293.25);
|
||||
// Testing minutes and seconds
|
||||
expect(stripErrorMargin(excelDateTimeSerial(new Date(Date.UTC(2024, 0, 2, 1, 2, 3))))).toBe(45293.0430902778);
|
||||
expect(stripErrorMargin(excelDateTimeSerial(new Date(Date.UTC(2024, 5, 24, 15, 10, 0))))).toBe(45467.6319444444);
|
||||
});
|
||||
|
||||
it('Function excelSerialToDate', () => {
|
||||
expect(formatDateDefault(excelSerialToDate(59))).toBe('1900/02/28');
|
||||
expect(formatDateDefault(excelSerialToDate(61))).toBe('1900/03/01');
|
||||
expect(formatDateDefault(excelSerialToDate(367))).toBe('1901/01/01');
|
||||
expect(formatDateDefault(excelSerialToDate(45324))).toBe('2024/02/02');
|
||||
expect(formatDateDefault(excelSerialToDate(0, DateSystem.Date1904))).toBe('1904/01/01');
|
||||
expect(excelDateSerial(new Date(Date.UTC(1904, 0, 2)), DateSystem.Date1904)).toBe(1);
|
||||
});
|
||||
it('Function isValidDateStr', () => {
|
||||
expect(isValidDateStr('2020-1-1')).toBeTruthy();
|
||||
expect(isValidDateStr('2020/1/31')).toBeTruthy();
|
||||
expect(isValidDateStr('2020-2-31')).toBeFalsy();
|
||||
expect(isValidDateStr('2020/001/31')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('uses the decoded 1904 date when day-count calculations receive serial zero', () => {
|
||||
expect(getTwoDateDaysByBasis(0, 1, 0, DateSystem.Date1904).days).toBe(1);
|
||||
expect(getTwoDateDaysByBasis(0, 1, 1, DateSystem.Date1904)).toEqual({ days: 1, yearDays: 366 });
|
||||
expect(getTwoDateDaysByBasis(0, 1, 4, DateSystem.Date1904).days).toBe(1);
|
||||
});
|
||||
|
||||
it('validates the maximum serial against the active Excel date system', () => {
|
||||
const invalid1900 = getDateSerialNumberByObject(NumberValueObject.create(2958466), DateSystem.Date1900);
|
||||
const invalid1904 = getDateSerialNumberByObject(NumberValueObject.create(2957004), DateSystem.Date1904);
|
||||
|
||||
expect(getDateSerialNumberByObject(NumberValueObject.create(2958465), DateSystem.Date1900)).toBe(2958465);
|
||||
expect(typeof invalid1900 === 'number' ? invalid1900 : invalid1900.getValue()).toBe(ErrorType.NUM);
|
||||
expect(getDateSerialNumberByObject(NumberValueObject.create(2957003), DateSystem.Date1904)).toBe(2957003);
|
||||
expect(typeof invalid1904 === 'number' ? invalid1904 : invalid1904.getValue()).toBe(ErrorType.NUM);
|
||||
});
|
||||
});
|
||||
57
packages/engine-formula/src/basics/__tests__/dirty.spec.ts
Normal file
57
packages/engine-formula/src/basics/__tests__/dirty.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IUnitRange } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isInDirtyRange } from '../dirty';
|
||||
|
||||
describe('dirty range matching', () => {
|
||||
const dirtyRanges: IUnitRange[] = [
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
range: {
|
||||
startRow: 2,
|
||||
endRow: 4,
|
||||
startColumn: 1,
|
||||
endColumn: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-2',
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('matches cells inside the edited range including boundary cells', () => {
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-1', 'sheet-1', 2, 1)).toBe(true);
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-1', 'sheet-1', 3, 2)).toBe(true);
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-1', 'sheet-1', 4, 3)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not match cells outside the range or from another sheet/workbook', () => {
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-1', 'sheet-1', 1, 1)).toBe(false);
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-1', 'sheet-1', 4, 4)).toBe(false);
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-1', 'sheet-2', 3, 2)).toBe(false);
|
||||
expect(isInDirtyRange(dirtyRanges, 'unit-2', 'sheet-1', 3, 2)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 { Complex } from '../complex';
|
||||
import { BESSEL } from '../engineering';
|
||||
|
||||
describe('Test ref regex', () => {
|
||||
it('BESSEL.besselj test', () => {
|
||||
expect(BESSEL.besselj(3, 0)).toStrictEqual(-0.2600519577199309);
|
||||
expect(BESSEL.besselj(9, 0)).toStrictEqual(-0.09033361101567323);
|
||||
expect(BESSEL.besselj(9, 1)).toStrictEqual(0.2453117865662281);
|
||||
expect(BESSEL.besselj(0, 1)).toStrictEqual(0);
|
||||
expect(BESSEL.besselj(0, 2)).toStrictEqual(0);
|
||||
expect(BESSEL.besselj(-1, 1)).toStrictEqual(-0.4400505856771301);
|
||||
expect(BESSEL.besselj(3, 2)).toStrictEqual(0.4860912632349548);
|
||||
expect(BESSEL.besselj(2, 3)).toStrictEqual(0.12894324997562717);
|
||||
expect(BESSEL.besselj(2, 300)).toStrictEqual(Number.NaN);
|
||||
});
|
||||
|
||||
it('BESSEL.bessely test', () => {
|
||||
expect(BESSEL.bessely(3, 0)).toStrictEqual(0.3768500150996341);
|
||||
expect(BESSEL.bessely(9, 0)).toStrictEqual(0.24993669828517454);
|
||||
expect(BESSEL.bessely(9, 1)).toStrictEqual(0.10431457495919716);
|
||||
});
|
||||
|
||||
it('BESSEL.besseli test', () => {
|
||||
expect(BESSEL.besseli(4, 4)).toStrictEqual(1.4162757349797126);
|
||||
expect(BESSEL.besseli(4, 0)).toStrictEqual(11.301922170199603);
|
||||
expect(BESSEL.besseli(4, 1)).toStrictEqual(9.759465157394443);
|
||||
expect(BESSEL.besseli(1, 0)).toStrictEqual(1.2660658480342601);
|
||||
expect(BESSEL.besseli(0, 1)).toStrictEqual(0);
|
||||
expect(BESSEL.besseli(0, 2)).toStrictEqual(0);
|
||||
expect(BESSEL.besseli(2, 300)).toStrictEqual(Number.NaN);
|
||||
});
|
||||
|
||||
it('BESSEL.besselk test', () => {
|
||||
expect(BESSEL.besselk(3, 3)).toStrictEqual(0.12217037599313321);
|
||||
expect(BESSEL.besselk(3, 0)).toStrictEqual(0.03473950439930018);
|
||||
expect(BESSEL.besselk(1, 0)).toStrictEqual(0.421024421083418);
|
||||
});
|
||||
|
||||
it('Complex test createByComplexStr', () => {
|
||||
const complex = Complex.createByComplexStr(0, 1, 'i');
|
||||
const realNum = complex.getRealNum();
|
||||
const iNum = complex.getINum();
|
||||
const suffix = complex.getSuffix();
|
||||
const result = Complex.getComplex(realNum, iNum, suffix);
|
||||
expect(result).toStrictEqual('i');
|
||||
});
|
||||
|
||||
it('Complex test isError', () => {
|
||||
let complex = new Complex('1+1');
|
||||
expect(complex.isError()).toStrictEqual(true);
|
||||
|
||||
complex = new Complex('1+ti');
|
||||
expect(complex.isError()).toStrictEqual(true);
|
||||
|
||||
complex = new Complex('1-ti');
|
||||
expect(complex.isError()).toStrictEqual(true);
|
||||
|
||||
complex = new Complex('1+ttt');
|
||||
expect(complex.isError()).toStrictEqual(true);
|
||||
|
||||
complex = new Complex('ti');
|
||||
expect(complex.isError()).toStrictEqual(true);
|
||||
|
||||
complex = new Complex('+ti');
|
||||
expect(complex.isError()).toStrictEqual(true);
|
||||
|
||||
complex = new Complex('12i');
|
||||
expect(complex.isError()).toStrictEqual(false);
|
||||
|
||||
complex = new Complex('1-i');
|
||||
expect(complex.isError()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { InvertedIndexCache } from '../inverted-index-cache';
|
||||
|
||||
describe('InvertedIndexCache', () => {
|
||||
it('keeps empty string lookups distinct from numeric zero lookups', () => {
|
||||
const cache = new InvertedIndexCache();
|
||||
const unitId = 'unit';
|
||||
const sheetId = 'sheet';
|
||||
const column = 0;
|
||||
const rows: [number, number][] = [[0, 2]];
|
||||
|
||||
cache.setContinueBuildingCache(unitId, sheetId, column, 0, 2);
|
||||
cache.set(unitId, sheetId, column, 0, 0, true);
|
||||
cache.set(unitId, sheetId, column, '', 1, true);
|
||||
cache.set(unitId, sheetId, column, null, 2, true);
|
||||
|
||||
expect(cache.getCellPositions(unitId, sheetId, column, 0, rows)?.matchingRows).toEqual([0, 1, 2]);
|
||||
expect(cache.getCellPositions(unitId, sheetId, column, '', rows)?.matchingRows).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
34
packages/engine-formula/src/basics/__tests__/math.spec.ts
Normal file
34
packages/engine-formula/src/basics/__tests__/math.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { calculateCombin, calculateFactorial } from '../math';
|
||||
|
||||
describe('Test math', () => {
|
||||
it('Factorial test', () => {
|
||||
expect(calculateFactorial(0)).toBe(1);
|
||||
expect(calculateFactorial(1)).toBe(1);
|
||||
expect(calculateFactorial(1.9)).toBe(1);
|
||||
expect(calculateFactorial(5)).toBe(120);
|
||||
expect(calculateFactorial(-1)).toBe(Number.NaN);
|
||||
expect(calculateFactorial(171)).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('Combin test', () => {
|
||||
expect(calculateCombin(8, 2)).toBe(28);
|
||||
expect(calculateCombin(1000000, 2000)).toBe(Infinity);
|
||||
});
|
||||
});
|
||||
494
packages/engine-formula/src/basics/__tests__/regex.spec.ts
Normal file
494
packages/engine-formula/src/basics/__tests__/regex.spec.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
/**
|
||||
* 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 {
|
||||
isReferenceString,
|
||||
REFERENCE_TABLE_ALL_COLUMN_REGEX,
|
||||
REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX,
|
||||
REFERENCE_TABLE_SINGLE_COLUMN_REGEX,
|
||||
REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX,
|
||||
regexTestColumn,
|
||||
regexTestMultipleRange,
|
||||
regexTestRow,
|
||||
regexTestSingeRange,
|
||||
} from '../regex';
|
||||
|
||||
describe('Test ref regex', () => {
|
||||
it('Single range', () => {
|
||||
expect(regexTestSingeRange('A1')).toBe(true);
|
||||
expect(regexTestSingeRange('Sheet1!A1')).toBe(true);
|
||||
expect(regexTestSingeRange('[workbook]Sheet1!A1')).toBe(true);
|
||||
expect(regexTestSingeRange('[workbook]\'Sheet-1\'!A1')).toBe(true);
|
||||
expect(regexTestSingeRange('\'[workbook]Sheet1\'!A1')).toBe(true);
|
||||
});
|
||||
|
||||
it('Multiple range', () => {
|
||||
expect(regexTestMultipleRange('A1:B10')).toBe(true);
|
||||
expect(regexTestMultipleRange('Sheet1!A1:B10')).toBe(true);
|
||||
expect(regexTestMultipleRange('[workbook]Sheet1!A1:B10')).toBe(true);
|
||||
expect(regexTestMultipleRange('[workbook]\'Sheet-1\'!A1:B10')).toBe(true);
|
||||
expect(regexTestMultipleRange('\'[workbook]Sheet1\'!A1:B10')).toBe(true);
|
||||
});
|
||||
|
||||
it('Row range', () => {
|
||||
expect(regexTestRow('1:10')).toBe(true);
|
||||
expect(regexTestRow('Sheet1!1:10')).toBe(true);
|
||||
expect(regexTestRow('[workbook]Sheet1!1:10')).toBe(true);
|
||||
expect(regexTestRow('[workbook]\'Sheet-1\'!1:10')).toBe(true);
|
||||
expect(regexTestRow('\'[workbook]Sheet1\'!1:10')).toBe(true);
|
||||
});
|
||||
|
||||
it('Column range', () => {
|
||||
expect(regexTestColumn('A:B')).toBe(true);
|
||||
expect(regexTestColumn('Sheet1!A:B')).toBe(true);
|
||||
expect(regexTestColumn('[workbook]Sheet1!A:B')).toBe(true);
|
||||
expect(regexTestColumn('[workbook]\'Sheet-1\'!A:B')).toBe(true);
|
||||
expect(regexTestColumn('\'[workbook]Sheet1\'!A:B')).toBe(true);
|
||||
});
|
||||
|
||||
it('Table single range', () => {
|
||||
expect(new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX).test('Table1[[#Title],[#Data],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('allows a hash only at the end of a table column name', () => {
|
||||
const regex = new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX);
|
||||
|
||||
expect(regex.test('Table1[Column#]')).toBe(true);
|
||||
expect(regex.test('Table1[Column#Name]')).toBe(false);
|
||||
});
|
||||
|
||||
it('Table multiple range', () => {
|
||||
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('accepts escaped Univer display names as Table qualifiers', () => {
|
||||
const regex = new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX);
|
||||
|
||||
expect(regex.test('[Urban Nomad retail operations hub | Official showcase]!Inventory[Safety stock]')).toBe(true);
|
||||
expect(regex.test('[Base name with ]] bracket]!Inventory[Safety stock]')).toBe(true);
|
||||
});
|
||||
|
||||
it('isReferenceString', () => {
|
||||
expect(isReferenceString('A1')).toBeTruthy();
|
||||
expect(isReferenceString('Sheet1!A1')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]Sheet1!A1')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]\'Sheet-1\'!A1')).toBeTruthy();
|
||||
expect(isReferenceString('\'[workbook]Sheet1\'!A1')).toBeTruthy();
|
||||
|
||||
expect(isReferenceString('A1:B10')).toBeTruthy();
|
||||
expect(isReferenceString('Sheet1!A1:B10')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]Sheet1!A1:B10')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]\'Sheet-1\'!A1:B10')).toBeTruthy();
|
||||
expect(isReferenceString('\'[workbook]Sheet1\'!A1:B10')).toBeTruthy();
|
||||
|
||||
expect(isReferenceString('1:10')).toBeTruthy();
|
||||
expect(isReferenceString('Sheet1!1:10')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]Sheet1!1:10')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]\'Sheet-1\'!1:10')).toBeTruthy();
|
||||
expect(isReferenceString('\'[workbook]Sheet1\'!1:10')).toBeTruthy();
|
||||
|
||||
expect(isReferenceString('A:B')).toBeTruthy();
|
||||
expect(isReferenceString('Sheet1!A:B')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]Sheet1!A:B')).toBeTruthy();
|
||||
expect(isReferenceString('[workbook]\'Sheet-1\'!A:B')).toBeTruthy();
|
||||
expect(isReferenceString('\'[workbook]Sheet1\'!A:B')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Structured Reference extra tests (comments in English)
|
||||
// Assumes REFERENCE_TABLE_SINGLE_COLUMN_REGEX / REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX are already imported.
|
||||
describe('Structured Reference – Single Column (positive)', () => {
|
||||
const RE_SINGLE = new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX);
|
||||
|
||||
it('matches basic single column', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[Column1]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches double-bracket single column', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches with #Headers tag', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Headers],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches with #Data tag', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Data],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches with #All tag', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#All],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches with #Totals tag', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Totals],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches with #This Row tag', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#This Row],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches with #Title tag (allowed in your grammar)', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Title],[Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches tag order swapped (column before tag)', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[Column1],[#Data]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches spaces around tokens', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[ Column1 ]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches spaces and comma spacing', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Data] , [Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches mixed spacing inside brackets', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[ #All ],[ Column1 ]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches unicode/space column name', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[客户 名称]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches special char column name', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[Order-ID]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches line-break column name', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[CASH\r\nOUT]')).toBe(true);
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Totals],[CASH\r\nOUT]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches pure unicode column name', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[中文列名]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches empty bracket', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[]')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Structured Reference – Single Column (negative)', () => {
|
||||
const RE_SINGLE = new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX);
|
||||
|
||||
it('does not match #All only', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[#All]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match #Headers only', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[#Headers]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match #Data only', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[#Data]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match #Totals only', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[#Totals]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match table name only', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Structured Reference – Multi Column (positive)', () => {
|
||||
const RE_MULTI = new RegExp(REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX);
|
||||
|
||||
it('matches basic column range', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with #Headers', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Headers],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with #Data', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Data],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with #All', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#All],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with #Totals', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Totals],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with #This Row', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#This Row],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with #Title', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Title],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with multi tags', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Headers],[#Data],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with spaces around tokens', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[ Column1 ]:[ Column10 ]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches column range with spaced colon and tag', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Data], [ Column1 ] : [ Column10 ]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches line-break column names in range', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[CASH\r\nOUT]:[CASH\r\nIN]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches unicode/special char names in range', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[客户 名称]:[订单-ID]]')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Structured Reference – Multi Column (negative)', () => {
|
||||
const RE_MULTI = new RegExp(REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX);
|
||||
|
||||
it('does not match single column simple', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[Column1]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match single column with tag', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Data],[Column1]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match comma-separated multiple columns (list, not range)', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[Column1],[Column2]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match comma-separated with tag', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Data],[Column1],[Column3]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match #All only', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[#All]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match #Headers only', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[#Headers]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match A1-style range', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[A1:B10]')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match empty bracket', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[]')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Structured Reference – Cross guard', () => {
|
||||
const RE_MULTI = new RegExp(REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX);
|
||||
|
||||
it('multi-regex should not match single-column (double bracket)', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[Column1]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('multi-regex should not match single-column (simple)', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[Column1]')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Structured Reference – Order & spacing edge cases', () => {
|
||||
const RE_SINGLE = new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX);
|
||||
const RE_MULTI = new RegExp(REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX);
|
||||
|
||||
// Single
|
||||
it('single: tag order after column (Totals)', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[Column1],[#Totals]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('single: multiple tags after column', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[Column1],[#Headers],[#Data]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('single: excessive spaces around comma', () => {
|
||||
RE_SINGLE.lastIndex = 0;
|
||||
expect(RE_SINGLE.test('Table1[[#Data] , [Column1]]')).toBe(true);
|
||||
});
|
||||
|
||||
// Multi
|
||||
it('multi: tag after range', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[Column1]:[Column10],[#All]]')).toBe(false);
|
||||
});
|
||||
|
||||
it('multi: multiple tags before range', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#Data],[#Totals],[Column1]:[Column10]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('multi: excessive spaces around colon and tags', () => {
|
||||
RE_MULTI.lastIndex = 0;
|
||||
expect(RE_MULTI.test('Table1[[#All] , [Column1] : [Column10]]')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single all column test', () => {
|
||||
const FULL_SINGLE = new RegExp(REFERENCE_TABLE_ALL_COLUMN_REGEX);
|
||||
|
||||
// Should match a plain table name (shorthand for #All)
|
||||
it('matches plain table name', () => {
|
||||
FULL_SINGLE.lastIndex = 0;
|
||||
expect(FULL_SINGLE.test('Table1')).toBe(true);
|
||||
});
|
||||
|
||||
// Should not match an empty bracket (invalid structured reference)
|
||||
it('rejects empty brackets', () => {
|
||||
FULL_SINGLE.lastIndex = 0;
|
||||
expect(FULL_SINGLE.test('Table1[]')).toBe(false);
|
||||
});
|
||||
|
||||
// Should not match strings with non-table prefix
|
||||
it('rejects prefixed string', () => {
|
||||
FULL_SINGLE.lastIndex = 0;
|
||||
expect(FULL_SINGLE.test('122Table1')).toBe(true);
|
||||
});
|
||||
|
||||
// Should not match column-qualified reference (not a pure whole-table reference)
|
||||
it('rejects table with column reference', () => {
|
||||
FULL_SINGLE.lastIndex = 0;
|
||||
expect(FULL_SINGLE.test('Table1[Column1]')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Title-only hash test', () => {
|
||||
const TITLE_ONLY = new RegExp(REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX);
|
||||
|
||||
// Should match a basic title-only structured reference, e.g. Table1[#Headers]
|
||||
it('matches simple hash title', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#Headers]')).toBe(true);
|
||||
});
|
||||
|
||||
// Should match arbitrary hash names, not only Excel built-ins
|
||||
it('matches any #title content', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#FooBar]')).toBe(true);
|
||||
});
|
||||
|
||||
// Should allow spaces inside the bracket
|
||||
it('matches hash title with spaces', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#This Row]')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts external Unit qualifiers', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
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
|
||||
it('rejects normal column name', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[Column1]')).toBe(false);
|
||||
});
|
||||
|
||||
// Should reject missing content after #
|
||||
it('rejects # with no content', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#]')).toBe(false);
|
||||
});
|
||||
|
||||
// Should reject nested blocks (multiple bracket groups)
|
||||
it('rejects multiple bracket segments', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#Headers][#Data]')).toBe(false);
|
||||
});
|
||||
|
||||
// Should reject empty bracket
|
||||
it('rejects empty bracket', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[]')).toBe(false);
|
||||
});
|
||||
|
||||
// Should reject malformed formats
|
||||
it('rejects malformed hash block', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#Headers')).toBe(false); // missing ]
|
||||
});
|
||||
|
||||
// Should reject trailing characters after the bracket
|
||||
it('rejects trailing characters', () => {
|
||||
TITLE_ONLY.lastIndex = 0;
|
||||
expect(TITLE_ONLY.test('Table1[#Headers]Extra')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
54
packages/engine-formula/src/basics/cache-lru.ts
Normal file
54
packages/engine-formula/src/basics/cache-lru.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { hashAlgorithm, LRUMap } from '@univerjs/core';
|
||||
|
||||
export class FormulaAstLRU<T> {
|
||||
private _cache: LRUMap<string, T>;
|
||||
|
||||
constructor(cacheCount: number) {
|
||||
this._cache = new LRUMap<string, T>(cacheCount);
|
||||
}
|
||||
|
||||
set(formulaString: string, node: T) {
|
||||
const hash = this._hash(formulaString);
|
||||
this._cache.set(hash, node);
|
||||
}
|
||||
|
||||
get(formulaString: string) {
|
||||
const hash = this._hash(formulaString);
|
||||
return this._cache.get(hash);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
delete(formulaString: string) {
|
||||
this._cache.delete(this._hash(formulaString));
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, key: string, map: LRUMap<string, T>) => void, thisArg?: any) {
|
||||
this._cache.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
private _hash(formulaString: string) {
|
||||
if (formulaString.length <= 64) {
|
||||
return formulaString;
|
||||
}
|
||||
return hashAlgorithm(formulaString).toString();
|
||||
}
|
||||
}
|
||||
42
packages/engine-formula/src/basics/calculate.ts
Normal file
42
packages/engine-formula/src/basics/calculate.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { compareToken } from './token';
|
||||
|
||||
export function reverseCompareOperator(operator: compareToken): compareToken {
|
||||
let result: compareToken;
|
||||
switch (operator) {
|
||||
case compareToken.EQUALS:
|
||||
result = compareToken.EQUALS;
|
||||
break;
|
||||
case compareToken.GREATER_THAN:
|
||||
result = compareToken.LESS_THAN;
|
||||
break;
|
||||
case compareToken.GREATER_THAN_OR_EQUAL:
|
||||
result = compareToken.LESS_THAN_OR_EQUAL;
|
||||
break;
|
||||
case compareToken.LESS_THAN:
|
||||
result = compareToken.GREATER_THAN;
|
||||
break;
|
||||
case compareToken.LESS_THAN_OR_EQUAL:
|
||||
result = compareToken.GREATER_THAN_OR_EQUAL;
|
||||
break;
|
||||
case compareToken.NOT_EQUAL:
|
||||
result = compareToken.NOT_EQUAL;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
284
packages/engine-formula/src/basics/common.ts
Normal file
284
packages/engine-formula/src/basics/common.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 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,
|
||||
IColumnData,
|
||||
IObjectArrayPrimitiveType,
|
||||
IObjectMatrixPrimitiveType,
|
||||
IRange,
|
||||
IRowData,
|
||||
IUnitRange,
|
||||
Nullable,
|
||||
ObjectMatrix,
|
||||
Styles,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import type { sequenceNodeType } from '../engine/utils/sequence';
|
||||
import type { IImageFormulaInfo } from '../engine/value-object/primitive-object';
|
||||
|
||||
export const ERROR_VALUE_OBJECT_CLASS_TYPE = 'errorValueObject';
|
||||
|
||||
export const ASYNC_OBJECT_CLASS_TYPE = 'asyncObject';
|
||||
|
||||
export const REFERENCE_OBJECT_CLASS_TYPE = 'referenceObject';
|
||||
|
||||
export const VALUE_OBJECT_CLASS_TYPE = 'valueObject';
|
||||
|
||||
export enum BooleanValue {
|
||||
FALSE = 'FALSE',
|
||||
TRUE = 'TRUE',
|
||||
}
|
||||
|
||||
export enum AstNodePromiseType {
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export interface ISheetItem {
|
||||
cellData: ObjectMatrix<ICellData>;
|
||||
rowCount: number;
|
||||
columnCount: number;
|
||||
rowData: IObjectArrayPrimitiveType<Partial<IRowData>>;
|
||||
columnData: IObjectArrayPrimitiveType<Partial<IColumnData>>;
|
||||
defaultColumnWidth?: number;
|
||||
defaultRowHeight?: number;
|
||||
}
|
||||
|
||||
export interface ISheetData {
|
||||
[sheetId: string]: ISheetItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* The subset of workbook data needs to be assembled into a new reference object when being passed in,
|
||||
* and then input through the FormulaCurrentConfigService.
|
||||
*/
|
||||
export interface IUnitData {
|
||||
[unitId: string]: ISheetData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Style data, numfmt needs to be queried from the style sheet
|
||||
*/
|
||||
export interface IUnitStylesData {
|
||||
[unitId: string]: Styles;
|
||||
}
|
||||
|
||||
export interface IRuntimeUnitDataType {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: ObjectMatrix<Nullable<ICellData>> }>;
|
||||
}
|
||||
|
||||
export interface IRuntimeUnitDataPrimitiveType {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<Nullable<ICellData>> }>;
|
||||
}
|
||||
|
||||
export interface IRuntimeOtherUnitDataType {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: Nullable<{ [formulaId: string]: IObjectMatrixPrimitiveType<Nullable<ICellData>[][]> }> }>;
|
||||
}
|
||||
|
||||
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 IFormulaExternalReferenceBinding {
|
||||
qualifier: string;
|
||||
sourceUnitId: string;
|
||||
sourceUnitType: FormulaUnitType;
|
||||
}
|
||||
|
||||
export interface IFormulaExternalReferenceResource {
|
||||
schemaVersion: number;
|
||||
references: Record<string, IFormulaExternalReferenceBinding>;
|
||||
}
|
||||
|
||||
/** Host-scoped External Reference resources keyed by the Host unitId. */
|
||||
export interface IFormulaExternalReferences {
|
||||
[hostUnitId: string]: IFormulaExternalReferenceResource;
|
||||
}
|
||||
|
||||
export interface IUnitSheetIdToNameMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: string }>;
|
||||
}
|
||||
|
||||
export interface IDirtyUnitSheetNameMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: string }>;
|
||||
}
|
||||
|
||||
export interface IDirtyUnitDefinedNameMap {
|
||||
[unitId: string]: Nullable<{ [definedName: string]: string }>;
|
||||
}
|
||||
|
||||
export interface IDirtyUnitSuperTableMap {
|
||||
[unitId: string]: Nullable<{ [tableName: string]: string }>;
|
||||
}
|
||||
|
||||
export interface IDirtyUnitFeatureMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: { [featureId: string]: boolean } }>;
|
||||
}
|
||||
|
||||
export interface IDirtyUnitOtherFormulaMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: { [formulaId: string]: boolean } }>;
|
||||
}
|
||||
|
||||
export interface IArrayFormulaRangeType {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<IRange> }>;
|
||||
}
|
||||
|
||||
export interface IArrayFormulaEmbeddedMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<boolean> }>;
|
||||
}
|
||||
|
||||
export interface IFeatureDirtyRangeType {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IRange[] }>;
|
||||
}
|
||||
|
||||
export interface IRuntimeImageFormulaDataType extends IImageFormulaInfo {
|
||||
unitId: string;
|
||||
sheetId: string;
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface IUnitImageFormulaDataType {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: ObjectMatrix<Nullable<IImageFormulaInfo>> }>;
|
||||
}
|
||||
|
||||
export interface IArrayFormulaUnitCellType extends IRuntimeUnitDataPrimitiveType { }
|
||||
|
||||
export interface IFormulaData {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: Nullable<IObjectMatrixPrimitiveType<Nullable<IFormulaDataItem>>> }>;
|
||||
}
|
||||
|
||||
export interface IFormulaIdMap {
|
||||
f: string;
|
||||
r: number;
|
||||
c: number;
|
||||
}
|
||||
|
||||
export interface IFormulaStringMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<string[]> }>;
|
||||
}
|
||||
|
||||
export interface IFormulaExecuteResultItem {
|
||||
value: Nullable<number | string | boolean | Array<Array<number | string | boolean | null>>>;
|
||||
formula: string;
|
||||
}
|
||||
|
||||
export interface IFormulaExecuteResultMap {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<IFormulaExecuteResultItem[]> }>;
|
||||
}
|
||||
|
||||
export interface IFormulaIdMapData {
|
||||
[unitId: string]: Nullable<{ [subUnitId: string]: Nullable<{ [formulaId: string]: IFormulaIdMap }> }>;
|
||||
}
|
||||
|
||||
export interface IOtherFormulaData {
|
||||
[unitId: string]: Nullable<{ [subUnitId: string]: Nullable<{ [formulaId: string]: IOtherFormulaDataItem }> }>;
|
||||
}
|
||||
/**
|
||||
* @f formulaString, the text string of the formula.
|
||||
* @si The formula ID can be utilized in scenarios such as copy-pasting and drag-filling to convert formulas into references, eliminating the need for recreating the formulaString.
|
||||
*/
|
||||
export interface IFormulaDataItem {
|
||||
f: string; // formulaString
|
||||
x?: number; // Offset from x direction
|
||||
y?: number; // Offset from y direction
|
||||
si?: string; // formulaId,
|
||||
// row: number;
|
||||
// column: number;
|
||||
// sheetId: string;
|
||||
}
|
||||
|
||||
export interface IOtherFormulaDataItem {
|
||||
f: string; // formulaString
|
||||
ranges: IRange[];
|
||||
}
|
||||
|
||||
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;
|
||||
/** Whether the projected range contains a physical totals row. */
|
||||
showFooter?: boolean;
|
||||
}
|
||||
|
||||
export enum TableOptionType {
|
||||
ALL = '#All',
|
||||
DATA = '#Data',
|
||||
HEADERS = '#Headers',
|
||||
TOTALS = '#Totals',
|
||||
THIS_ROW = '#This Row',
|
||||
}
|
||||
|
||||
export interface IUnitExcludedCell {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: ObjectMatrix<boolean> }>;
|
||||
}
|
||||
|
||||
export interface IUnitRowData {
|
||||
[unitId: string]: Nullable<{ [sheetId: string]: IObjectArrayPrimitiveType<Partial<IRowData>> }>;
|
||||
}
|
||||
|
||||
export interface IFormulaDatasetConfig {
|
||||
formulaData: IFormulaData;
|
||||
arrayFormulaCellData: IArrayFormulaUnitCellType;
|
||||
arrayFormulaRange: IArrayFormulaRangeType;
|
||||
forceCalculate: boolean;
|
||||
dirtyRanges: IUnitRange[];
|
||||
dirtyNameMap: IDirtyUnitSheetNameMap;
|
||||
dirtyDefinedNameMap: IDirtyUnitDefinedNameMap;
|
||||
dirtySuperTableMap?: IDirtyUnitSuperTableMap;
|
||||
dirtyUnitFeatureMap: IDirtyUnitFeatureMap;
|
||||
dirtyUnitOtherFormulaMap: IDirtyUnitOtherFormulaMap;
|
||||
clearDependencyTreeCache?: IUnitSheetIdToNameMap;
|
||||
excludedCell?: IUnitExcludedCell;
|
||||
allUnitData?: IUnitData;
|
||||
unitStylesData?: IUnitStylesData;
|
||||
unitSheetNameMap?: IUnitSheetNameMap;
|
||||
unitNameMap?: IFormulaUnitNameMap;
|
||||
externalReferences?: IFormulaExternalReferences;
|
||||
maxIteration?: number;
|
||||
isCalculateTreeModel?: boolean;
|
||||
rowData?: IUnitRowData; // Include rows hidden by filters
|
||||
}
|
||||
|
||||
export enum ConcatenateType {
|
||||
FRONT,
|
||||
BACK,
|
||||
}
|
||||
|
||||
export interface IExprTreeNode {
|
||||
value: string;
|
||||
children: IExprTreeNode[];
|
||||
startIndex: number;
|
||||
type?: sequenceNodeType;
|
||||
}
|
||||
608
packages/engine-formula/src/basics/complex.ts
Normal file
608
packages/engine-formula/src/basics/complex.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* 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 Decimal from 'decimal.js';
|
||||
|
||||
// Decimal.cos has a bug when calculating PI/2
|
||||
Decimal.prototype.cos = function (): Decimal {
|
||||
const num = this.toNumber();
|
||||
return new Decimal(Math.cos(num));
|
||||
};
|
||||
|
||||
export class Complex {
|
||||
static getComplex(realNum: number, iNum: number, suffix: string): number | string {
|
||||
const _realNum = new Decimal(realNum).toSignificantDigits(15).toNumber();
|
||||
const _iNum = new Decimal(iNum).toSignificantDigits(15).toNumber();
|
||||
const _suffix = suffix === '' ? 'i' : suffix;
|
||||
|
||||
let result: number | string;
|
||||
|
||||
if (_realNum === 0 && _iNum === 0) {
|
||||
result = 0;
|
||||
} else if (_realNum === 0) {
|
||||
result = _iNum === 1 ? _suffix : `${_iNum}${_suffix}`;
|
||||
} else if (_iNum === 0) {
|
||||
result = _realNum;
|
||||
} else {
|
||||
const sign = _iNum > 0 ? '+' : '';
|
||||
const suffixStr = _iNum === 1 ? _suffix : `${_iNum}${_suffix}`;
|
||||
result = `${_realNum}${sign}${suffixStr}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static createByComplexStr(realNum: number, iNum: number, suffix: string): Complex {
|
||||
const complexStr = Complex.getComplex(realNum, iNum, suffix);
|
||||
|
||||
return new Complex(complexStr);
|
||||
}
|
||||
|
||||
private _inumber: string | number = '';
|
||||
|
||||
private _realNum: number = 0;
|
||||
|
||||
private _iNum: number = 0;
|
||||
|
||||
private _suffix: string = '';
|
||||
|
||||
private _isError: boolean = false;
|
||||
|
||||
constructor(inumber: number | string) {
|
||||
if (`${inumber}`.trim() === '') {
|
||||
this._isError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._inumber = inumber;
|
||||
this._getImReal();
|
||||
this._getImAginary();
|
||||
this._getImSuffix();
|
||||
}
|
||||
|
||||
private _getImReal(): void {
|
||||
if (this._inumber === 0 || this._inumber === '0') {
|
||||
this._realNum = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const inumberStr = `${this._inumber}`;
|
||||
|
||||
if (['i', '+i', '1i', '+1i', '-i', '-1i', 'j', '+j', '1j', '+1j', '-j', '-1j'].indexOf(inumberStr) >= 0) {
|
||||
this._realNum = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let plus = inumberStr.indexOf('+');
|
||||
let minus = inumberStr.indexOf('-');
|
||||
|
||||
if (plus === 0) {
|
||||
plus = inumberStr.indexOf('+', 1);
|
||||
}
|
||||
|
||||
if (minus === 0) {
|
||||
minus = inumberStr.indexOf('-', 1);
|
||||
}
|
||||
|
||||
const last = inumberStr.substring(inumberStr.length - 1, inumberStr.length);
|
||||
const unit = last === 'i' || last === 'j';
|
||||
|
||||
if (plus >= 0 || minus >= 0) {
|
||||
if (!unit) {
|
||||
this._isError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (plus >= 0) {
|
||||
if (Number.isNaN(+inumberStr.substring(0, plus)) || Number.isNaN(+inumberStr.substring(plus + 1, inumberStr.length - 1))) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._realNum = +inumberStr.substring(0, plus);
|
||||
}
|
||||
} else {
|
||||
if (Number.isNaN(+inumberStr.substring(0, minus)) || Number.isNaN(+inumberStr.substring(minus + 1, inumberStr.length - 1))) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._realNum = +inumberStr.substring(0, minus);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (unit) {
|
||||
if (Number.isNaN(+inumberStr.substring(0, inumberStr.length - 1))) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._realNum = 0;
|
||||
}
|
||||
} else {
|
||||
if (Number.isNaN(+inumberStr)) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._realNum = +inumberStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getImAginary(): void {
|
||||
if (this._isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._inumber === 0 || this._inumber === '0') {
|
||||
this._iNum = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let inumberStr = `${this._inumber}`;
|
||||
|
||||
if (['i', 'j'].indexOf(inumberStr) >= 0) {
|
||||
this._iNum = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
inumberStr = inumberStr.replace('+i', '+1i').replace('-i', '-1i').replace('+j', '+1j').replace('-j', '-1j');
|
||||
|
||||
let plus = inumberStr.indexOf('+');
|
||||
let minus = inumberStr.indexOf('-');
|
||||
|
||||
if (plus === 0) {
|
||||
plus = inumberStr.indexOf('+', 1);
|
||||
}
|
||||
|
||||
if (minus === 0) {
|
||||
minus = inumberStr.indexOf('-', 1);
|
||||
}
|
||||
|
||||
const last = inumberStr.substring(inumberStr.length - 1, inumberStr.length);
|
||||
const unit = last === 'i' || last === 'j';
|
||||
|
||||
if (plus >= 0 || minus >= 0) {
|
||||
if (!unit) {
|
||||
this._isError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (plus >= 0) {
|
||||
if (Number.isNaN(+inumberStr.substring(0, plus)) || Number.isNaN(+inumberStr.substring(plus + 1, inumberStr.length - 1))) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._iNum = +inumberStr.substring(plus + 1, inumberStr.length - 1);
|
||||
}
|
||||
} else {
|
||||
if (Number.isNaN(+inumberStr.substring(0, minus)) || Number.isNaN(+inumberStr.substring(minus + 1, inumberStr.length - 1))) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._iNum = -(+inumberStr.substring(minus + 1, inumberStr.length - 1));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (unit) {
|
||||
if (Number.isNaN(+inumberStr.substring(0, inumberStr.length - 1))) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._iNum = +inumberStr.substring(0, inumberStr.length - 1);
|
||||
}
|
||||
} else {
|
||||
if (Number.isNaN(+inumberStr)) {
|
||||
this._isError = true;
|
||||
} else {
|
||||
this._iNum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getImSuffix(): void {
|
||||
const inumberStr = `${this._inumber}`;
|
||||
|
||||
const suffix = inumberStr.substring(inumberStr.length - 1);
|
||||
|
||||
this._suffix = suffix === 'i' || suffix === 'j' ? suffix : '';
|
||||
}
|
||||
|
||||
getRealNum(): number {
|
||||
return this._realNum;
|
||||
}
|
||||
|
||||
getINum(): number {
|
||||
return this._iNum;
|
||||
}
|
||||
|
||||
getSuffix(): string {
|
||||
return this._suffix;
|
||||
}
|
||||
|
||||
isError(): boolean {
|
||||
return this._isError;
|
||||
}
|
||||
|
||||
toString(): number | string {
|
||||
return Complex.getComplex(this._realNum, this._iNum, this._suffix);
|
||||
}
|
||||
|
||||
isDifferentSuffixes(complex2: Complex): boolean {
|
||||
const suffix2 = complex2.getSuffix();
|
||||
|
||||
if (this._suffix === '' || suffix2 === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._suffix !== suffix2;
|
||||
}
|
||||
|
||||
Abs(): number {
|
||||
const result = Decimal.sqrt(Decimal.pow(this._realNum, 2).add(Decimal.pow(this._iNum, 2))).toSignificantDigits(16).toNumber();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Argument(): number {
|
||||
const abs = Decimal.sqrt(Decimal.pow(this._realNum, 2).add(Decimal.pow(this._iNum, 2)));
|
||||
|
||||
let result = Decimal.acos(new Decimal(this._realNum).div(abs)).toSignificantDigits(16).toNumber();
|
||||
|
||||
if (this._iNum < 0) {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Conjugate(): number | string {
|
||||
return Complex.getComplex(this._realNum, -this._iNum, this._suffix);
|
||||
}
|
||||
|
||||
Cos(): number | string {
|
||||
if (this._iNum) {
|
||||
const realNum = Decimal.cos(this._realNum).mul(Decimal.cosh(this._iNum)).toNumber();
|
||||
const iNum = Decimal.sin(this._realNum).mul(Decimal.sinh(this._iNum)).negated().toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = Decimal.cos(this._realNum).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Cosh(): number | string {
|
||||
if (!Number.isFinite(Math.sinh(this._realNum)) || !Number.isFinite(Math.cosh(this._realNum))) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this._iNum) {
|
||||
const realNum = Decimal.cosh(this._realNum).mul(Decimal.cos(this._iNum)).toNumber();
|
||||
const iNum = Decimal.sinh(this._realNum).mul(Decimal.sin(this._iNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = Decimal.cosh(this._realNum).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Cot(): number | string {
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._iNum * 2).sub(Decimal.cos(this._realNum * 2));
|
||||
const realNum = Decimal.sin(this._realNum * 2).div(den).toNumber();
|
||||
const iNum = Decimal.sinh(this._iNum * 2).div(den).negated().toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = new Decimal(1).div(Decimal.tan(this._realNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Coth(): number | string {
|
||||
if (!Number.isFinite(Math.sinh(this._realNum)) || !Number.isFinite(Math.cosh(this._realNum))) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._realNum * 2).sub(Decimal.cos(this._iNum * 2));
|
||||
const realNum = Decimal.sinh(this._realNum * 2).div(den).toNumber();
|
||||
const iNum = Decimal.sin(this._iNum * 2).div(den).negated().toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = new Decimal(1).div(Decimal.tanh(this._realNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Csc(): number | string {
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._iNum * 2).sub(Decimal.cos(this._realNum * 2));
|
||||
const realNum = Decimal.sin(this._realNum).mul(Decimal.cosh(this._iNum)).mul(2).div(den).toNumber();
|
||||
const iNum = Decimal.cos(this._realNum).mul(Decimal.sinh(this._iNum)).mul(-2).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = new Decimal(1).div(Decimal.sin(this._realNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Csch(): number | string {
|
||||
if (!Number.isFinite(Math.sinh(this._realNum)) || !Number.isFinite(Math.cosh(this._realNum))) {
|
||||
return Complex.getComplex(0, 0, this._suffix);
|
||||
}
|
||||
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._realNum * 2).sub(Decimal.cos(this._iNum * 2));
|
||||
const realNum = Decimal.sinh(this._realNum).mul(Decimal.cos(this._iNum)).mul(2).div(den).toNumber();
|
||||
const iNum = Decimal.cosh(this._realNum).mul(Decimal.sin(this._iNum)).mul(-2).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = new Decimal(1).div(Decimal.sinh(this._realNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Div(complex2: Complex): number | string {
|
||||
const Decimal_realNum1 = new Decimal(this._realNum);
|
||||
const Decimal_iNum1 = new Decimal(this._iNum);
|
||||
const Decimal_realNum2 = new Decimal(complex2.getRealNum());
|
||||
const Decimal_iNum2 = new Decimal(complex2.getINum());
|
||||
|
||||
const den = Decimal_realNum2.mul(Decimal_realNum2).add(Decimal_iNum2.mul(Decimal_iNum2));
|
||||
const realNum = Decimal_realNum1.mul(Decimal_realNum2).add(Decimal_iNum1.mul(Decimal_iNum2)).div(den).toNumber();
|
||||
const iNum = Decimal_iNum1.mul(Decimal_realNum2).sub(Decimal_realNum1.mul(Decimal_iNum2)).div(den).toNumber();
|
||||
|
||||
const suffix = this._suffix === '' ? complex2.getSuffix() : this._suffix;
|
||||
|
||||
return Complex.getComplex(realNum, iNum, suffix);
|
||||
}
|
||||
|
||||
Exp(): number | string {
|
||||
if (!Number.isFinite(Math.exp(this._realNum))) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
const realNum = Decimal.exp(this._realNum).mul(Decimal.cos(this._iNum)).toNumber();
|
||||
const iNum = Decimal.exp(this._realNum).mul(Decimal.sin(this._iNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
}
|
||||
|
||||
Ln(): number | string {
|
||||
const abs = Decimal.sqrt(Decimal.pow(this._realNum, 2).add(Decimal.pow(this._iNum, 2)));
|
||||
const realNum = Decimal.ln(abs).toNumber();
|
||||
const iNum = Decimal.acos(new Decimal(this._realNum).div(abs)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
}
|
||||
|
||||
Log(base: number): number | string {
|
||||
const abs = Decimal.sqrt(Decimal.pow(this._realNum, 2).add(Decimal.pow(this._iNum, 2)));
|
||||
const Decimal_realNum1 = Decimal.ln(abs);
|
||||
let Decimal_iNum1 = Decimal.acos(new Decimal(this._realNum).div(abs));
|
||||
|
||||
if (this._iNum < 0) {
|
||||
Decimal_iNum1 = Decimal_iNum1.negated();
|
||||
}
|
||||
|
||||
const Decimal_realNum2 = Decimal.ln(base);
|
||||
const Decimal_iNum2 = new Decimal(0);
|
||||
|
||||
const den = Decimal_realNum2.mul(Decimal_realNum2).add(Decimal_iNum2.mul(Decimal_iNum2));
|
||||
|
||||
if (den.eq(0)) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
const realNum = Decimal_realNum1.mul(Decimal_realNum2).add(Decimal_iNum1.mul(Decimal_iNum2)).div(den).toNumber();
|
||||
const iNum = Decimal_iNum1.mul(Decimal_realNum2).sub(Decimal_realNum1.mul(Decimal_iNum2)).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
}
|
||||
|
||||
Power(number: number): number | string {
|
||||
if (this._realNum === 0 && this._iNum === 0) {
|
||||
if (number > 0) {
|
||||
return Complex.getComplex(this._realNum, this._iNum, this._suffix);
|
||||
} else {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
let power = Decimal.sqrt(Decimal.pow(this._realNum, 2).add(Decimal.pow(this._iNum, 2)));
|
||||
let phi = Decimal.acos(new Decimal(this._realNum).div(power));
|
||||
|
||||
if (this._iNum < 0) {
|
||||
phi = phi.negated();
|
||||
}
|
||||
|
||||
power = Decimal.pow(power, number);
|
||||
phi = phi.mul(number);
|
||||
|
||||
const realNum = Decimal.cos(phi).mul(power).toNumber();
|
||||
const iNum = Decimal.sin(phi).mul(power).toNumber();
|
||||
|
||||
if (!Number.isFinite(realNum) || !Number.isFinite(iNum)) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
}
|
||||
|
||||
Product(complex2: Complex): number | string {
|
||||
const Decimal_realNum1 = new Decimal(this._realNum);
|
||||
const Decimal_iNum1 = new Decimal(this._iNum);
|
||||
const Decimal_realNum2 = new Decimal(complex2.getRealNum());
|
||||
const Decimal_iNum2 = new Decimal(complex2.getINum());
|
||||
|
||||
const realNum = Decimal_realNum1.mul(Decimal_realNum2).sub(Decimal_iNum1.mul(Decimal_iNum2)).toNumber();
|
||||
const iNum = Decimal_realNum1.mul(Decimal_iNum2).add(Decimal_iNum1.mul(Decimal_realNum2)).toNumber();
|
||||
|
||||
const suffix = this._suffix === '' ? complex2.getSuffix() : this._suffix;
|
||||
|
||||
return Complex.getComplex(realNum, iNum, suffix);
|
||||
}
|
||||
|
||||
Sec(): number | string {
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._iNum * 2).add(Decimal.cos(this._realNum * 2));
|
||||
const realNum = Decimal.cos(this._realNum).mul(Decimal.cosh(this._iNum)).mul(2).div(den).toNumber();
|
||||
const iNum = Decimal.sin(this._realNum).mul(Decimal.sinh(this._iNum)).mul(2).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = new Decimal(1).div(Decimal.cos(this._realNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Sech(): number | string {
|
||||
if (!Number.isFinite(Math.sinh(this._realNum * 2)) || !Number.isFinite(Math.cosh(this._realNum * 2))) {
|
||||
return Complex.getComplex(0, 0, this._suffix);
|
||||
}
|
||||
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._realNum * 2).add(Decimal.cos(this._iNum * 2));
|
||||
const realNum = Decimal.cosh(this._realNum).mul(Decimal.cos(this._iNum)).mul(2).div(den).toNumber();
|
||||
const iNum = Decimal.sinh(this._realNum).mul(Decimal.sin(this._iNum)).mul(-2).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = new Decimal(1).div(Decimal.cosh(this._realNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Sin(): number | string {
|
||||
if (this._iNum) {
|
||||
const realNum = Decimal.sin(this._realNum).mul(Decimal.cosh(this._iNum)).toNumber();
|
||||
const iNum = Decimal.cos(this._realNum).mul(Decimal.sinh(this._iNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = Decimal.sin(this._realNum).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Sinh(): number | string {
|
||||
if (!Number.isFinite(Math.sinh(this._realNum)) || !Number.isFinite(Math.cosh(this._realNum))) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this._iNum) {
|
||||
const realNum = Decimal.sinh(this._realNum).mul(Decimal.cos(this._iNum)).toNumber();
|
||||
const iNum = Decimal.cosh(this._realNum).mul(Decimal.sin(this._iNum)).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = Decimal.sinh(this._realNum).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Sqrt(): number | string {
|
||||
const abs = Decimal.sqrt(Decimal.pow(this._realNum, 2).add(Decimal.pow(this._iNum, 2)));
|
||||
const abs_sqrt = Decimal.sqrt(abs);
|
||||
|
||||
let arg = Decimal.acos(new Decimal(this._realNum).div(abs));
|
||||
|
||||
if (this._iNum < 0) {
|
||||
arg = arg.negated();
|
||||
}
|
||||
|
||||
const realNum = abs_sqrt.mul(Decimal.cos(arg.div(2).toNumber())).toNumber();
|
||||
const iNum = abs_sqrt.mul(Decimal.sin(arg.div(2))).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
}
|
||||
|
||||
Sub(complex2: Complex): number | string {
|
||||
const Decimal_realNum1 = new Decimal(this._realNum);
|
||||
const Decimal_iNum1 = new Decimal(this._iNum);
|
||||
const Decimal_realNum2 = new Decimal(complex2.getRealNum());
|
||||
const Decimal_iNum2 = new Decimal(complex2.getINum());
|
||||
|
||||
const realNum = Decimal_realNum1.sub(Decimal_realNum2).toNumber();
|
||||
const iNum = Decimal_iNum1.sub(Decimal_iNum2).toNumber();
|
||||
|
||||
const suffix = this._suffix === '' ? complex2.getSuffix() : this._suffix;
|
||||
|
||||
return Complex.getComplex(realNum, iNum, suffix);
|
||||
}
|
||||
|
||||
Sum(complex2: Complex): number | string {
|
||||
const Decimal_realNum1 = new Decimal(this._realNum);
|
||||
const Decimal_iNum1 = new Decimal(this._iNum);
|
||||
const Decimal_realNum2 = new Decimal(complex2.getRealNum());
|
||||
const Decimal_iNum2 = new Decimal(complex2.getINum());
|
||||
|
||||
const realNum = Decimal_realNum1.add(Decimal_realNum2).toNumber();
|
||||
const iNum = Decimal_iNum1.add(Decimal_iNum2).toNumber();
|
||||
|
||||
const suffix = this._suffix === '' ? complex2.getSuffix() : this._suffix;
|
||||
|
||||
return Complex.getComplex(realNum, iNum, suffix);
|
||||
}
|
||||
|
||||
Tan(): number | string {
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cos(this._realNum * 2).add(Decimal.cosh(this._iNum * 2));
|
||||
const realNum = Decimal.sin(this._realNum * 2).div(den).toNumber();
|
||||
const iNum = Decimal.sinh(this._iNum * 2).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = Decimal.tan(this._realNum).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
|
||||
Tanh(): number | string {
|
||||
if (!Number.isFinite(Math.sinh(this._realNum)) || !Number.isFinite(Math.cosh(this._realNum))) {
|
||||
this._isError = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this._iNum) {
|
||||
const den = Decimal.cosh(this._realNum * 2).add(Decimal.cos(this._iNum * 2));
|
||||
const realNum = Decimal.sinh(this._realNum * 2).div(den).toNumber();
|
||||
const iNum = Decimal.sin(this._iNum * 2).div(den).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, iNum, this._suffix);
|
||||
} else {
|
||||
const realNum = Decimal.tanh(this._realNum).toNumber();
|
||||
|
||||
return Complex.getComplex(realNum, this._iNum, this._suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
282
packages/engine-formula/src/basics/database.ts
Normal file
282
packages/engine-formula/src/basics/database.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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 { DateSystem } from '@univerjs/core';
|
||||
import type { ArrayValueObject } from '../engine/value-object/array-value-object';
|
||||
import type { BaseValueObject } from '../engine/value-object/base-value-object';
|
||||
import { isRealNum } from '@univerjs/core';
|
||||
import { findCompareToken } from '../engine/utils/object-compare';
|
||||
import { ValueObjectFactory } from '../engine/value-object/array-value-object';
|
||||
import { ErrorValueObject } from '../engine/value-object/base-value-object';
|
||||
import { ErrorType } from './error-type';
|
||||
|
||||
type DatabaseValueType = string | number | null;
|
||||
|
||||
export function checkDatabase(database: BaseValueObject) {
|
||||
const databaseValues: DatabaseValueType[][] = [];
|
||||
|
||||
if (database.isError()) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: database as ErrorValueObject,
|
||||
databaseValues,
|
||||
};
|
||||
}
|
||||
|
||||
const rowCount = database.isArray() ? (database as ArrayValueObject).getRowCount() : 1;
|
||||
const columnCount = database.isArray() ? (database as ArrayValueObject).getColumnCount() : 1;
|
||||
|
||||
if (rowCount < 2) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: ErrorValueObject.create(ErrorType.VALUE),
|
||||
databaseValues,
|
||||
};
|
||||
}
|
||||
|
||||
for (let r = 0; r < rowCount; r++) {
|
||||
const row: DatabaseValueType[] = [];
|
||||
|
||||
for (let c = 0; c < columnCount; c++) {
|
||||
const valueObject = (database as ArrayValueObject).get(r, c) as BaseValueObject;
|
||||
|
||||
if (valueObject.isNull()) {
|
||||
row.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = `${valueObject.getValue()}`;
|
||||
|
||||
if (valueObject.isBoolean()) {
|
||||
value = value.toLocaleUpperCase();
|
||||
}
|
||||
|
||||
if (valueObject.isNumber() || isRealNum(value)) {
|
||||
row.push(+value);
|
||||
continue;
|
||||
}
|
||||
|
||||
row.push(value);
|
||||
}
|
||||
|
||||
databaseValues.push(row);
|
||||
}
|
||||
|
||||
return {
|
||||
isError: false,
|
||||
errorObject: null,
|
||||
databaseValues,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkField(field: BaseValueObject, database: DatabaseValueType[][]) {
|
||||
let fieldIndex: number = -1;
|
||||
|
||||
if (field.isError()) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: field as ErrorValueObject,
|
||||
fieldIndex,
|
||||
};
|
||||
}
|
||||
|
||||
const rowCount = field.isArray() ? (field as ArrayValueObject).getRowCount() : 1;
|
||||
const columnCount = field.isArray() ? (field as ArrayValueObject).getColumnCount() : 1;
|
||||
|
||||
if (rowCount > 1 || columnCount > 1) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: ErrorValueObject.create(ErrorType.VALUE),
|
||||
fieldIndex,
|
||||
};
|
||||
}
|
||||
|
||||
const fieldObject = field.isArray() ? (field as ArrayValueObject).get(0, 0) as BaseValueObject : field;
|
||||
|
||||
let fieldValue: number | string = `${fieldObject.getValue()}`;
|
||||
|
||||
if (fieldObject.isNull()) {
|
||||
fieldValue = 0;
|
||||
} else if (fieldObject.isBoolean()) {
|
||||
fieldValue = fieldObject.getValue() ? 1 : 0;
|
||||
} else if (fieldObject.isNumber() || isRealNum(fieldValue)) {
|
||||
fieldValue = Math.floor(+fieldValue);
|
||||
}
|
||||
|
||||
if (typeof fieldValue === 'number') {
|
||||
if (fieldValue < 1 || fieldValue > database[0].length) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: ErrorValueObject.create(ErrorType.VALUE),
|
||||
fieldIndex,
|
||||
};
|
||||
}
|
||||
|
||||
fieldIndex = fieldValue - 1;
|
||||
} else {
|
||||
fieldIndex = database[0].findIndex((value) => {
|
||||
if (value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return `${value}`.toLocaleLowerCase() === fieldValue.toLocaleLowerCase();
|
||||
});
|
||||
|
||||
if (fieldIndex === -1) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: ErrorValueObject.create(ErrorType.VALUE),
|
||||
fieldIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isError: false,
|
||||
errorObject: null,
|
||||
fieldIndex,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkCriteria(criteria: BaseValueObject) {
|
||||
const criteriaValues: DatabaseValueType[][] = [];
|
||||
|
||||
if (criteria.isError()) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: criteria as ErrorValueObject,
|
||||
criteriaValues,
|
||||
};
|
||||
}
|
||||
|
||||
const rowCount = criteria.isArray() ? (criteria as ArrayValueObject).getRowCount() : 1;
|
||||
const columnCount = criteria.isArray() ? (criteria as ArrayValueObject).getColumnCount() : 1;
|
||||
|
||||
if (rowCount < 2) {
|
||||
return {
|
||||
isError: true,
|
||||
errorObject: ErrorValueObject.create(ErrorType.VALUE),
|
||||
criteriaValues,
|
||||
};
|
||||
}
|
||||
|
||||
for (let r = 0; r < rowCount; r++) {
|
||||
const row: DatabaseValueType[] = [];
|
||||
|
||||
for (let c = 0; c < columnCount; c++) {
|
||||
const valueObject = (criteria as ArrayValueObject).get(r, c) as BaseValueObject;
|
||||
|
||||
if (valueObject.isNull()) {
|
||||
row.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = `${valueObject.getValue()}`;
|
||||
|
||||
if (valueObject.isBoolean()) {
|
||||
row.push(valueObject.getValue() ? 1 : 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueObject.isNumber() || isRealNum(value)) {
|
||||
row.push(+value);
|
||||
continue;
|
||||
}
|
||||
|
||||
row.push(value);
|
||||
}
|
||||
|
||||
criteriaValues.push(row);
|
||||
}
|
||||
|
||||
return {
|
||||
isError: false,
|
||||
errorObject: null,
|
||||
criteriaValues,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Criteria match logic:
|
||||
* Boolean AND in row direction, then Boolean OR in column direction
|
||||
*/
|
||||
export function isCriteriaMatch(
|
||||
criteria: DatabaseValueType[][],
|
||||
database: DatabaseValueType[][],
|
||||
databaseRowIndex: number,
|
||||
dateSystem: DateSystem
|
||||
) {
|
||||
const rowCount = criteria.length;
|
||||
const columnCount = criteria[0].length;
|
||||
|
||||
const criteriaTitleIndexCache: Record<number, number> = {};
|
||||
|
||||
let isMatch = false;
|
||||
|
||||
for (let r = 1; r < rowCount; r++) {
|
||||
let isRowMatch = true;
|
||||
|
||||
for (let c = 0; c < columnCount; c++) {
|
||||
const criteriaValue = criteria[r][c];
|
||||
|
||||
if (criteriaValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let criteriaTitleIndex = criteriaTitleIndexCache[c];
|
||||
|
||||
if (criteriaTitleIndex === undefined) {
|
||||
const criteriaTitleValue = criteria[0][c];
|
||||
criteriaTitleIndex = database[0].findIndex((value) => {
|
||||
if (value === null || criteriaTitleValue === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return `${value}`.toLocaleLowerCase() === `${criteriaTitleValue}`.toLocaleLowerCase();
|
||||
});
|
||||
criteriaTitleIndexCache[c] = criteriaTitleIndex;
|
||||
}
|
||||
|
||||
if (criteriaTitleIndex === -1 && (typeof criteriaValue === 'string' || criteriaValue === 0)) {
|
||||
isRowMatch = false;
|
||||
break;
|
||||
} else if (criteriaTitleIndex > -1) {
|
||||
const databaseValue = database[databaseRowIndex][criteriaTitleIndex];
|
||||
|
||||
if (databaseValue === null) {
|
||||
isRowMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const [compareToken, criteriaObject] = findCompareToken(`${criteriaValue}`, dateSystem);
|
||||
const compareObject = ValueObjectFactory.create(`${databaseValue}`, false, dateSystem).compare(criteriaObject, compareToken);
|
||||
const compareValue = compareObject.getValue();
|
||||
|
||||
if (!compareValue) {
|
||||
isRowMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRowMatch) {
|
||||
isMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
550
packages/engine-formula/src/basics/date.ts
Normal file
550
packages/engine-formula/src/basics/date.ts
Normal file
@@ -0,0 +1,550 @@
|
||||
/**
|
||||
* 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 { BaseValueObject } from '../engine/value-object/base-value-object';
|
||||
import {
|
||||
DateSystem,
|
||||
excelDateSerial,
|
||||
excelDateTimeSerial,
|
||||
excelSerialToDate,
|
||||
getDateSystemMaxSerial,
|
||||
isRealNum,
|
||||
numfmt,
|
||||
} from '@univerjs/core';
|
||||
import { ErrorValueObject } from '../engine/value-object/base-value-object';
|
||||
import { ErrorType } from './error-type';
|
||||
|
||||
export const DEFAULT_DATE_FORMAT = 'yyyy/mm/dd;@';
|
||||
export const DEFAULT_NOW_FORMAT = 'yyyy/mm/dd hh:mm';
|
||||
export const DEFAULT_TIME_FORMAT = 'h:mm A/P';
|
||||
|
||||
export function formatDateDefault(date: Date): string {
|
||||
// Get the year from the date object
|
||||
const year: number = date.getFullYear();
|
||||
|
||||
// Get the month from the date object and add 1 (since getMonth() returns 0-11)
|
||||
// Convert it to a string and pad with zero if necessary to ensure two digits
|
||||
const month: string = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
|
||||
// Get the day from the date object
|
||||
// Convert it to a string and pad with zero if necessary to ensure two digits
|
||||
const day: string = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
// Concatenate year, month, and day with '/' as separator to form yyyy/mm/dd format
|
||||
return `${year}/${month}/${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate date string
|
||||
*
|
||||
* TODO @Dushusir: Internationalization and more format support, can be reused when editing and saving cells, like "2020年1月1日"
|
||||
*/
|
||||
export function isValidDateStr(dateStr: string): boolean {
|
||||
// Regular expression to validate date format
|
||||
const regex = /^\d{4}[-/](0?[1-9]|1[012])[-/](0?[1-9]|[12][0-9]|3[01])$/;
|
||||
|
||||
// Check if the date format is correct
|
||||
if (!regex.test(dateStr)) {
|
||||
return false;
|
||||
}
|
||||
// Convert date string to local time format
|
||||
const normalizedDateStr = dateStr.replace(/-/g, '/').replace(/T.+/, '');
|
||||
const dateWithTime = new Date(`${normalizedDateStr}`);
|
||||
|
||||
// Check if the date is valid
|
||||
if (Number.isNaN(dateWithTime.getTime())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert the parsed date back to the same format as the original date string for comparison
|
||||
const year = dateWithTime.getFullYear();
|
||||
const month = (dateWithTime.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = dateWithTime.getDate().toString().padStart(2, '0');
|
||||
const reconstructedDateStr = `${year}-${month}-${day}`;
|
||||
|
||||
const dateStrPad = dateStr.replace(/\//g, '-').split('-').map((v) => v.padStart(2, '0')).join('-');
|
||||
|
||||
return dateStrPad === reconstructedDateStr;
|
||||
}
|
||||
|
||||
export function parseFormattedDate(value: string, options?: { dateSystem?: DateSystem }) {
|
||||
return numfmt.parseDate(value, options);
|
||||
}
|
||||
|
||||
export function parseFormattedTime(value: string, options?: { dateSystem?: DateSystem }) {
|
||||
return numfmt.parseTime(value, options);
|
||||
}
|
||||
|
||||
function parseEnglishMonthNameDate(value: string): Date | null {
|
||||
const match = value.trim().match(/^(\d{1,2})\s*([A-Za-z]{3,9})\s*(\d{2,4})$/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, dayText, monthText, yearText] = match;
|
||||
const monthMap: Record<string, number> = {
|
||||
jan: 0,
|
||||
january: 0,
|
||||
feb: 1,
|
||||
february: 1,
|
||||
mar: 2,
|
||||
march: 2,
|
||||
apr: 3,
|
||||
april: 3,
|
||||
may: 4,
|
||||
jun: 5,
|
||||
june: 5,
|
||||
jul: 6,
|
||||
july: 6,
|
||||
aug: 7,
|
||||
august: 7,
|
||||
sep: 8,
|
||||
sept: 8,
|
||||
september: 8,
|
||||
oct: 9,
|
||||
october: 9,
|
||||
nov: 10,
|
||||
november: 10,
|
||||
dec: 11,
|
||||
december: 11,
|
||||
};
|
||||
const month = monthMap[monthText.toLowerCase()];
|
||||
|
||||
if (month == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const day = Number(dayText);
|
||||
const year = Number(yearText.length === 2 ? `20${yearText}` : yearText);
|
||||
const date = new Date(Date.UTC(year, month, day));
|
||||
|
||||
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export function isDate(format: string) {
|
||||
return numfmt.getFormatInfo(format).isDate;
|
||||
}
|
||||
|
||||
// Weekend is a weekend number or string that specifies when weekends occur.
|
||||
const weekendNumberMap: {
|
||||
[index: number]: number[];
|
||||
} = {
|
||||
1: [6, 0], // Saturday, Sunday
|
||||
2: [0, 1], // Sunday, Monday
|
||||
3: [1, 2], // Monday, Tuesday
|
||||
4: [2, 3], // Tuesday, Wednesday
|
||||
5: [3, 4], // Wednesday, Thursday
|
||||
6: [4, 5], // Thursday, Friday
|
||||
7: [5, 6], // Friday, Saturday
|
||||
11: [0], // Sunday only
|
||||
12: [1], // Monday only
|
||||
13: [2], // Tuesday only
|
||||
14: [3], // Wednesday only
|
||||
15: [4], // Thursday only
|
||||
16: [5], // Friday only
|
||||
17: [6], // Saturday only
|
||||
};
|
||||
|
||||
export function isValidWeekend(weekend: number | string): boolean {
|
||||
// Weekend string values are seven characters long and each character in the string represents a day of the week, starting with Monday.
|
||||
if (typeof weekend === 'string' && /^[0|1]{7}/.test(weekend)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (weekendNumberMap[Number(weekend)]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getWeekendArray(weekend: number | string): number[] {
|
||||
if (!isValidWeekend(weekend)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof weekend === 'string' && /^[0|1]{7}/.test(weekend)) {
|
||||
// 1 represents a non-workday and 0 represents a workday. Only the characters 1 and 0 are permitted in the string. Using 1111111 will always return 0.
|
||||
const result = [];
|
||||
|
||||
for (let i = 1; i <= weekend.length; i++) {
|
||||
if (`${weekend[i - 1]}` === '1') {
|
||||
if (i === weekend.length) {
|
||||
result.push(0);
|
||||
} else {
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return weekendNumberMap[Number(weekend)] || [];
|
||||
}
|
||||
|
||||
export function countWorkingDays(startDateSerialNumber: number, endDateSerialNumber: number, weekend: number | string = 1, holidays?: number[], dateSystem = DateSystem.Date1900): number {
|
||||
const weekendArray = getWeekendArray(weekend);
|
||||
|
||||
const start = Math.floor(startDateSerialNumber);
|
||||
const end = Math.floor(endDateSerialNumber);
|
||||
const startSerialNumber = end > start ? start : end;
|
||||
|
||||
let workingDays = 0;
|
||||
|
||||
const daysDiff = Math.abs(Math.floor(endDateSerialNumber) - Math.floor(startDateSerialNumber)) + 1;
|
||||
|
||||
for (let i = 0; i < daysDiff; i++) {
|
||||
const currentDateSerialNumber = startSerialNumber + i;
|
||||
|
||||
if (holidays && holidays.length > 0 && holidays.some((item) => Math.floor(item) === currentDateSerialNumber)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const weekDay = getWeekDayByDateSerialNumber(currentDateSerialNumber, dateSystem);
|
||||
|
||||
if (weekendArray.includes(weekDay)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
workingDays++;
|
||||
}
|
||||
|
||||
return end >= start ? workingDays : -workingDays;
|
||||
}
|
||||
|
||||
export function getDateSerialNumberByWorkingDays(startDateSerialNumber: number, workingDays: number, weekend: number | string = 1, holidays?: number[], dateSystem = DateSystem.Date1900): number | ErrorValueObject {
|
||||
const weekendArray = getWeekendArray(weekend);
|
||||
|
||||
const _startDateSerialNumber = Math.floor(startDateSerialNumber);
|
||||
let targetDateSerialNumber = _startDateSerialNumber;
|
||||
|
||||
let days = Math.abs(workingDays);
|
||||
|
||||
for (let i = 1; i <= days; i++) {
|
||||
const currentDateSerialNumber = workingDays < 0 ? _startDateSerialNumber - i : _startDateSerialNumber + i;
|
||||
|
||||
if (currentDateSerialNumber < 0) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
if (holidays && holidays.length > 0 && holidays.some((item) => Math.floor(item) === currentDateSerialNumber)) {
|
||||
days++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const weekDay = getWeekDayByDateSerialNumber(currentDateSerialNumber, dateSystem);
|
||||
|
||||
if (weekendArray.includes(weekDay)) {
|
||||
days++;
|
||||
continue;
|
||||
}
|
||||
|
||||
targetDateSerialNumber = currentDateSerialNumber;
|
||||
}
|
||||
|
||||
return targetDateSerialNumber;
|
||||
}
|
||||
|
||||
export function getDateSerialNumberByObject(
|
||||
serialNumberObject: BaseValueObject,
|
||||
dateSystem?: DateSystem
|
||||
): ErrorValueObject | number {
|
||||
if (serialNumberObject.isError()) {
|
||||
return serialNumberObject as ErrorValueObject;
|
||||
}
|
||||
|
||||
const dateValue = serialNumberObject.getValue();
|
||||
const maxSerial = getDateSystemMaxSerial(dateSystem);
|
||||
|
||||
if (serialNumberObject.isString()) {
|
||||
let dateSerial: any;
|
||||
const options = { dateSystem };
|
||||
|
||||
if (parseFormattedDate(`${dateValue}`, options)) {
|
||||
dateSerial = parseFormattedDate(`${dateValue}`, options)!.v;
|
||||
} else if (parseFormattedTime(`${dateValue}`, options)) {
|
||||
dateSerial = parseFormattedTime(`${dateValue}`, options)!.v;
|
||||
} else if (parseEnglishMonthNameDate(`${dateValue}`)) {
|
||||
dateSerial = parseEnglishMonthNameDate(`${dateValue}`)!;
|
||||
} else if (isRealNum(dateValue)) {
|
||||
dateSerial = +dateValue;
|
||||
} else {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
if (dateSerial instanceof Date) {
|
||||
dateSerial = excelDateTimeSerial(dateSerial, dateSystem);
|
||||
}
|
||||
|
||||
if (+dateSerial < 0 || +dateSerial > maxSerial) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
return +dateSerial;
|
||||
} else {
|
||||
const dateSerial = +serialNumberObject.getValue();
|
||||
|
||||
if (dateSerial < 0 || dateSerial > maxSerial) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
return dateSerial;
|
||||
}
|
||||
}
|
||||
|
||||
export function getWeekDayByDateSerialNumber(dateSerialNumber: number, dateSystem = DateSystem.Date1900): number {
|
||||
const offset = dateSystem === DateSystem.Date1904 ? 5 : 6;
|
||||
return ((Math.floor(dateSerialNumber) + offset) % 7 + 7) % 7;
|
||||
}
|
||||
|
||||
interface ITwoDateDaysType {
|
||||
days: number;
|
||||
yearDays: number;
|
||||
}
|
||||
|
||||
export function getTwoDateDaysByBasis(startDateSerialNumber: number, endDateSerialNumber: number, basis: number, dateSystem = DateSystem.Date1900): ITwoDateDaysType {
|
||||
switch (basis) {
|
||||
case 0:
|
||||
// U.S. (NASD) method 30/360.
|
||||
return getDaysByNASD(startDateSerialNumber, endDateSerialNumber, dateSystem);
|
||||
case 1:
|
||||
// Actual/actual
|
||||
return getDaysByActual(startDateSerialNumber, endDateSerialNumber, dateSystem);
|
||||
case 2:
|
||||
// Actual/360
|
||||
return {
|
||||
days: Math.abs(endDateSerialNumber - startDateSerialNumber),
|
||||
yearDays: 360,
|
||||
};
|
||||
case 3:
|
||||
// Actual/365
|
||||
return {
|
||||
days: Math.abs(endDateSerialNumber - startDateSerialNumber),
|
||||
yearDays: 365,
|
||||
};
|
||||
case 4:
|
||||
// European method 30/360.
|
||||
return getDaysByEuropean(startDateSerialNumber, endDateSerialNumber, dateSystem);
|
||||
default:
|
||||
return {
|
||||
days: Math.abs(endDateSerialNumber - startDateSerialNumber),
|
||||
yearDays: 365,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getDaysByNASD(startDateSerialNumber: number, endDateSerialNumber: number, dateSystem: DateSystem): ITwoDateDaysType {
|
||||
const startDateDate = excelSerialToDate(startDateSerialNumber, dateSystem);
|
||||
const startIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && startDateSerialNumber === 0;
|
||||
const startYear = startIsDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
|
||||
const startMonth = startIsDate1900SerialZero ? 1 : startDateDate.getUTCMonth() + 1;
|
||||
let startDay = startIsDate1900SerialZero ? 0 : startDateDate.getUTCDate();
|
||||
|
||||
let endDateDate = excelSerialToDate(endDateSerialNumber, dateSystem);
|
||||
const endIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && endDateSerialNumber === 0;
|
||||
let endYear = endIsDate1900SerialZero ? 1900 : endDateDate.getUTCFullYear();
|
||||
let endMonth = endIsDate1900SerialZero ? 1 : endDateDate.getUTCMonth() + 1;
|
||||
let endDay = endIsDate1900SerialZero ? 0 : endDateDate.getUTCDate();
|
||||
|
||||
const startIsLastDayOfFebruary = startMonth === 2 && startDay === getDaysInMonth(startYear, startMonth - 1);
|
||||
|
||||
// If the starting date is the last day of a month, it becomes equal to the 30th day of the same month.
|
||||
// If the ending date is the last day of a month and the starting date is earlier than the 30th day of a month, the ending date becomes equal to the 1st day of the next month; otherwise the ending date becomes equal to the 30th day of the same month.
|
||||
if (startIsLastDayOfFebruary) {
|
||||
startDay = 30;
|
||||
} else if (startDay === 31) {
|
||||
startDay = 30;
|
||||
}
|
||||
|
||||
if (endMonth === 2 && endDay === getDaysInMonth(endYear, endMonth - 1) && startIsLastDayOfFebruary) {
|
||||
endDay = 30;
|
||||
} else if (endDay === 31) {
|
||||
if (startDay < 30) {
|
||||
endDateDate = excelSerialToDate(endDateSerialNumber + 1, dateSystem);
|
||||
endYear = endDateDate.getUTCFullYear();
|
||||
endMonth = endDateDate.getUTCMonth() + 1;
|
||||
endDay = endDateDate.getUTCDate();
|
||||
} else {
|
||||
endDay = 30;
|
||||
}
|
||||
}
|
||||
|
||||
const daysInYears = (endYear - startYear) * 360;
|
||||
const daysInStartMonth = endDateSerialNumber >= startDateSerialNumber ? 30 - startDay : -startDay;
|
||||
const daysInEndMonth = endDateSerialNumber >= startDateSerialNumber ? endDay : endDay - 30;
|
||||
const daysInMidMonths = (endDateSerialNumber >= startDateSerialNumber ? (endMonth - startMonth - 1) : (endMonth - startMonth + 1)) * 30;
|
||||
const totalDays = Math.abs(daysInYears + daysInStartMonth + daysInEndMonth + daysInMidMonths);
|
||||
|
||||
return {
|
||||
days: totalDays,
|
||||
yearDays: 360,
|
||||
};
|
||||
}
|
||||
|
||||
function getDaysByActual(startDateSerialNumber: number, endDateSerialNumber: number, dateSystem: DateSystem): ITwoDateDaysType {
|
||||
const startDateDate = excelSerialToDate(startDateSerialNumber, dateSystem);
|
||||
const startIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && startDateSerialNumber === 0;
|
||||
const startYear = startIsDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
|
||||
const startMonth = startIsDate1900SerialZero ? 1 : startDateDate.getUTCMonth() + 1;
|
||||
const startDay = startIsDate1900SerialZero ? 0 : startDateDate.getUTCDate();
|
||||
|
||||
const endDateDate = excelSerialToDate(endDateSerialNumber, dateSystem);
|
||||
const endIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && endDateSerialNumber === 0;
|
||||
const endYear = endIsDate1900SerialZero ? 1900 : endDateDate.getUTCFullYear();
|
||||
const endMonth = endIsDate1900SerialZero ? 1 : endDateDate.getUTCMonth() + 1;
|
||||
const endDay = endIsDate1900SerialZero ? 0 : endDateDate.getUTCDate();
|
||||
|
||||
const totalDays = Math.abs(endDateSerialNumber - startDateSerialNumber);
|
||||
|
||||
// A period from a date to the same date in a later year is a whole number of years.
|
||||
// Excel uses the actual length of those years instead of averaging both boundary years.
|
||||
if (endDateSerialNumber >= startDateSerialNumber && endYear > startYear && startMonth === endMonth && startDay === endDay) {
|
||||
return {
|
||||
days: totalDays,
|
||||
yearDays: totalDays / (endYear - startYear),
|
||||
};
|
||||
}
|
||||
|
||||
const totalYear = Math.abs(endYear - startYear) + 1;
|
||||
|
||||
let startYearFirstDaySerialNumber;
|
||||
let endYearLastDaySerialNumber;
|
||||
|
||||
if (endYear < startYear) {
|
||||
const startYearFirstDay = new Date(Date.UTC(endYear, 0, 1));
|
||||
const endYearLastDay = new Date(Date.UTC(startYear, 11, 31));
|
||||
|
||||
startYearFirstDaySerialNumber = excelDateSerial(startYearFirstDay, dateSystem);
|
||||
endYearLastDaySerialNumber = excelDateSerial(endYearLastDay, dateSystem);
|
||||
|
||||
if (dateSystem === DateSystem.Date1900 && endYear === 1900) { // Special handle. excel 1900 days = 365, 1900/12/31 SerialNumber = 366. so start add 1
|
||||
startYearFirstDaySerialNumber += 1;
|
||||
}
|
||||
} else {
|
||||
const startYearFirstDay = new Date(Date.UTC(startYear, 0, 1));
|
||||
const endYearLastDay = new Date(Date.UTC(endYear, 11, 31));
|
||||
|
||||
startYearFirstDaySerialNumber = excelDateSerial(startYearFirstDay, dateSystem);
|
||||
endYearLastDaySerialNumber = excelDateSerial(endYearLastDay, dateSystem);
|
||||
|
||||
if (dateSystem === DateSystem.Date1900 && startYear === 1900) { // Special handle. excel 1900 days = 365, 1900/12/31 SerialNumber = 366. so start add 1
|
||||
startYearFirstDaySerialNumber += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
days: totalDays,
|
||||
yearDays: (endYearLastDaySerialNumber - startYearFirstDaySerialNumber + 1) / totalYear,
|
||||
};
|
||||
}
|
||||
|
||||
function getDaysByEuropean(startDateSerialNumber: number, endDateSerialNumber: number, dateSystem: DateSystem): ITwoDateDaysType {
|
||||
const startDateDate = excelSerialToDate(startDateSerialNumber, dateSystem);
|
||||
const startIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && startDateSerialNumber === 0;
|
||||
const startYear = startIsDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
|
||||
const startMonth = startIsDate1900SerialZero ? 1 : startDateDate.getUTCMonth() + 1;
|
||||
let startDay = startIsDate1900SerialZero ? 0 : startDateDate.getUTCDate();
|
||||
|
||||
const endDateDate = excelSerialToDate(endDateSerialNumber, dateSystem);
|
||||
const endIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && endDateSerialNumber === 0;
|
||||
const endYear = endIsDate1900SerialZero ? 1900 : endDateDate.getUTCFullYear();
|
||||
const endMonth = endIsDate1900SerialZero ? 1 : endDateDate.getUTCMonth() + 1;
|
||||
let endDay = endIsDate1900SerialZero ? 0 : endDateDate.getUTCDate();
|
||||
|
||||
// Starting dates and ending dates that occur on the 31st day of a month become equal to the 30th day of the same month.
|
||||
if (startDay === 31) {
|
||||
startDay = 30;
|
||||
}
|
||||
|
||||
if (endDay === 31) {
|
||||
endDay = 30;
|
||||
}
|
||||
|
||||
const daysInYears = (endYear - startYear) * 360;
|
||||
const daysInStartMonth = endDateSerialNumber >= startDateSerialNumber ? 30 - startDay : -startDay;
|
||||
const daysInEndMonth = endDateSerialNumber >= startDateSerialNumber ? endDay : endDay - 30;
|
||||
const daysInMidMonths = (endDateSerialNumber >= startDateSerialNumber ? (endMonth - startMonth - 1) : (endMonth - startMonth + 1)) * 30;
|
||||
const totalDays = Math.abs(daysInYears + daysInStartMonth + daysInEndMonth + daysInMidMonths);
|
||||
|
||||
return {
|
||||
days: totalDays,
|
||||
yearDays: 360,
|
||||
};
|
||||
}
|
||||
|
||||
export function isLeapYear(year: number): boolean {
|
||||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
||||
}
|
||||
|
||||
export function isLeapYear1900(year: number): boolean {
|
||||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 || year === 1900;
|
||||
}
|
||||
|
||||
const daysInMonthL = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
const daysInMonthR = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
|
||||
// month is 0 based
|
||||
export function getDaysInMonth(year: number, month: number): number {
|
||||
return isLeapYear(year) ? daysInMonthL[month] : daysInMonthR[month];
|
||||
}
|
||||
|
||||
export function getDaysInYear(year: number): number {
|
||||
return isLeapYear1900(year) ? 366 : 365;
|
||||
}
|
||||
|
||||
export function getNormalYearDaysByBasis(dateSerialNumber: number, basis: number, dateSystem = DateSystem.Date1900): number {
|
||||
switch (basis) {
|
||||
case 0:
|
||||
case 2:
|
||||
case 4:
|
||||
return 360;
|
||||
case 1:
|
||||
return getDaysInYear(excelSerialToDate(dateSerialNumber, dateSystem).getUTCFullYear());
|
||||
case 3:
|
||||
return 365;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export function lastDayOfMonth(year: number, month: number, day: number): boolean {
|
||||
return getDaysInMonth(year, month) === day;
|
||||
}
|
||||
|
||||
export function dateAddMonths(date: Date, months: number): Date {
|
||||
let year = date.getUTCFullYear();
|
||||
let month = date.getUTCMonth();
|
||||
const day = date.getUTCDate();
|
||||
|
||||
if (lastDayOfMonth(year, month, day)) {
|
||||
date.setUTCDate(1);
|
||||
date.setUTCMonth(date.getUTCMonth() + months);
|
||||
|
||||
year = date.getUTCFullYear();
|
||||
month = date.getUTCMonth();
|
||||
date.setUTCDate(getDaysInMonth(year, month));
|
||||
} else {
|
||||
date.setUTCMonth(date.getUTCMonth() + months);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
44
packages/engine-formula/src/basics/dirty.ts
Normal file
44
packages/engine-formula/src/basics/dirty.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IUnitRange } from '@univerjs/core';
|
||||
|
||||
export function isInDirtyRange(
|
||||
dirtyRanges: IUnitRange[],
|
||||
unitId: string,
|
||||
sheetId: string,
|
||||
row: number,
|
||||
column: number
|
||||
) {
|
||||
for (let i = 0, len = dirtyRanges.length; i < len; i++) {
|
||||
const dirtyRange = dirtyRanges[i];
|
||||
if (unitId !== dirtyRange.unitId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sheetId !== dirtyRange.sheetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { startRow, startColumn, endRow, endColumn } = dirtyRange.range;
|
||||
|
||||
if (row >= startRow && row <= endRow && column >= startColumn && column <= endColumn) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
485
packages/engine-formula/src/basics/engineering.ts
Normal file
485
packages/engine-formula/src/basics/engineering.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line ts/no-namespace
|
||||
export namespace BESSEL {
|
||||
interface IFunctionType {
|
||||
(x: number): number;
|
||||
}
|
||||
|
||||
const W = 0.636619772;
|
||||
|
||||
function _horner(arr: number[], v: number): number {
|
||||
let z = 0;
|
||||
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
z = v * z + arr[i];
|
||||
}
|
||||
|
||||
return z;
|
||||
}
|
||||
|
||||
function _bessel_iter(x: number, n: number, f0: number, f1: number, sign: number): number {
|
||||
if (n === 0) {
|
||||
return f0;
|
||||
}
|
||||
|
||||
if (n === 1) {
|
||||
return f1;
|
||||
}
|
||||
|
||||
const tdx = 2 / x;
|
||||
let _f0 = f0;
|
||||
let _f1 = f1;
|
||||
let f2 = f1;
|
||||
|
||||
for (let o = 1; o < n; ++o) {
|
||||
f2 = _f1 * o * tdx + sign * _f0;
|
||||
_f0 = _f1;
|
||||
_f1 = f2;
|
||||
}
|
||||
|
||||
return f2;
|
||||
}
|
||||
|
||||
function _bessel_wrap(bessel0: IFunctionType, bessel1: IFunctionType, nonzero: number, sign: number) {
|
||||
return function bessel(x: number, n: number): number {
|
||||
if (nonzero) {
|
||||
if (x === 0) {
|
||||
return nonzero === 1 ? -Infinity : Infinity;
|
||||
} else if (x < 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
if (n === 0) {
|
||||
return bessel0(x);
|
||||
}
|
||||
|
||||
if (n === 1) {
|
||||
return bessel1(x);
|
||||
}
|
||||
|
||||
if (n < 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
const _n = n | 0;
|
||||
|
||||
const b0 = bessel0(x);
|
||||
const b1 = bessel1(x);
|
||||
|
||||
return _bessel_iter(x, _n, b0, b1, sign);
|
||||
};
|
||||
}
|
||||
|
||||
export const besselj = (() => {
|
||||
const b0_a1a = [-184.9052456, 77392.33017, -11214424.18, 651619640.7, -13362590354.0, 57568490574.0];
|
||||
const b0_a2a = [1.0, 267.8532712, 59272.64853, 9494680.718, 1029532985.0, 57568490411.0];
|
||||
const b0_a1b = [0.2093887211e-6, -0.2073370639e-5, 0.2734510407e-4, -0.1098628627e-2, 1.0];
|
||||
const b0_a2b = [-0.934935152e-7, 0.7621095161e-6, -0.6911147651e-5, 0.1430488765e-3, -0.1562499995e-1];
|
||||
|
||||
function bessel0(x: number): number {
|
||||
let a = 0;
|
||||
let a1 = 0;
|
||||
let a2 = 0;
|
||||
let y = x * x;
|
||||
|
||||
if (x < 8) {
|
||||
a1 = _horner(b0_a1a, y);
|
||||
a2 = _horner(b0_a2a, y);
|
||||
a = a1 / a2;
|
||||
} else {
|
||||
const xx = x - 0.785398164;
|
||||
y = 64 / y;
|
||||
a1 = _horner(b0_a1b, y);
|
||||
a2 = _horner(b0_a2b, y);
|
||||
a = Math.sqrt(W / x) * (Math.cos(xx) * a1 - Math.sin(xx) * a2 * 8 / x);
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
const b1_a1a = [-30.16036606, 15704.48260, -2972611.439, 242396853.1, -7895059235.0, 72362614232.0];
|
||||
const b1_a2a = [1.0, 376.9991397, 99447.43394, 18583304.74, 2300535178.0, 144725228442.0];
|
||||
const b1_a1b = [-0.240337019e-6, 0.2457520174e-5, -0.3516396496e-4, 0.183105e-2, 1.0];
|
||||
const b1_a2b = [0.105787412e-6, -0.88228987e-6, 0.8449199096e-5, -0.2002690873e-3, 0.04687499995];
|
||||
|
||||
function bessel1(x: number): number {
|
||||
let a = 0;
|
||||
let a1 = 0;
|
||||
let a2 = 0;
|
||||
let y = x * x;
|
||||
const xx = Math.abs(x) - 2.356194491;
|
||||
|
||||
if (Math.abs(x) < 8) {
|
||||
a1 = x * _horner(b1_a1a, y);
|
||||
a2 = _horner(b1_a2a, y);
|
||||
a = a1 / a2;
|
||||
} else {
|
||||
y = 64 / y;
|
||||
a1 = _horner(b1_a1b, y);
|
||||
a2 = _horner(b1_a2b, y);
|
||||
a = Math.sqrt(W / Math.abs(x)) * (Math.cos(xx) * a1 - Math.sin(xx) * a2 * 8 / Math.abs(x));
|
||||
|
||||
if (x < 0) {
|
||||
a = -a;
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
return function besselj(x: number, n: number): number {
|
||||
const _n = Math.round(n);
|
||||
|
||||
if (!Number.isFinite(x)) {
|
||||
return Number.isNaN(x) ? x : 0;
|
||||
}
|
||||
|
||||
if (_n < 0) {
|
||||
return ((_n % 2) ? -1 : 1) * besselj(x, -_n);
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
return ((_n % 2) ? -1 : 1) * besselj(-x, _n);
|
||||
}
|
||||
|
||||
if (_n === 0) {
|
||||
return bessel0(x);
|
||||
}
|
||||
|
||||
if (_n === 1) {
|
||||
return bessel1(x);
|
||||
}
|
||||
|
||||
if (x === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ret = 0.0;
|
||||
|
||||
if (x > _n) {
|
||||
ret = _bessel_iter(x, _n, bessel0(x), bessel1(x), -1);
|
||||
} else {
|
||||
const m = 2 * Math.floor((_n + Math.floor(Math.sqrt(40 * _n))) / 2);
|
||||
let jsum = false;
|
||||
let bjp = 0.0;
|
||||
let sum = 0.0;
|
||||
let bj = 1.0;
|
||||
let bjm = 0.0;
|
||||
const tox = 2 / x;
|
||||
|
||||
for (let j = m; j > 0; j--) {
|
||||
bjm = j * tox * bj - bjp;
|
||||
bjp = bj;
|
||||
bj = bjm;
|
||||
|
||||
if (Math.abs(bj) > 1e10) {
|
||||
bj *= 1e-10;
|
||||
bjp *= 1e-10;
|
||||
ret *= 1e-10;
|
||||
sum *= 1e-10;
|
||||
}
|
||||
|
||||
if (jsum) {
|
||||
sum += bj;
|
||||
}
|
||||
|
||||
jsum = !jsum;
|
||||
|
||||
if (j === _n) {
|
||||
ret = bjp;
|
||||
}
|
||||
|
||||
// After more than 100 iterations, the result is 0
|
||||
if ((m - j) > 100 && ret === 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
sum = 2.0 * sum - bj;
|
||||
ret /= sum;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
})();
|
||||
|
||||
export const bessely = (() => {
|
||||
const b0_a1a = [228.4622733, -86327.92757, 10879881.29, -512359803.6, 7062834065.0, -2957821389.0];
|
||||
const b0_a2a = [1.0, 226.1030244, 47447.26470, 7189466.438, 745249964.8, 40076544269.0];
|
||||
const b0_a1b = [0.2093887211e-6, -0.2073370639e-5, 0.2734510407e-4, -0.1098628627e-2, 1.0];
|
||||
const b0_a2b = [-0.934945152e-7, 0.7621095161e-6, -0.6911147651e-5, 0.1430488765e-3, -0.1562499995e-1];
|
||||
|
||||
function bessel0(x: number): number {
|
||||
let a = 0;
|
||||
let a1 = 0;
|
||||
let a2 = 0;
|
||||
let y = x * x;
|
||||
const xx = x - 0.785398164;
|
||||
|
||||
if (x < 8) {
|
||||
a1 = _horner(b0_a1a, y);
|
||||
a2 = _horner(b0_a2a, y);
|
||||
a = a1 / a2 + W * besselj(x, 0) * Math.log(x);
|
||||
} else {
|
||||
y = 64 / y;
|
||||
a1 = _horner(b0_a1b, y);
|
||||
a2 = _horner(b0_a2b, y);
|
||||
a = Math.sqrt(W / x) * (Math.sin(xx) * a1 + Math.cos(xx) * a2 * 8 / x);
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
const b1_a1a = [0.8511937935e4, -0.4237922726e7, 0.7349264551e9, -0.5153438139e11, 0.1275274390e13, -0.4900604943e13];
|
||||
const b1_a2a = [1, 0.3549632885e3, 0.1020426050e6, 0.2245904002e8, 0.3733650367e10, 0.4244419664e12, 0.2499580570e14];
|
||||
const b1_a1b = [-0.240337019e-6, 0.2457520174e-5, -0.3516396496e-4, 0.183105e-2, 1.0];
|
||||
const b1_a2b = [0.105787412e-6, -0.88228987e-6, 0.8449199096e-5, -0.2002690873e-3, 0.04687499995];
|
||||
|
||||
function bessel1(x: number): number {
|
||||
let a = 0;
|
||||
let a1 = 0;
|
||||
let a2 = 0;
|
||||
let y = x * x;
|
||||
const xx = x - 2.356194491;
|
||||
|
||||
if (x < 8) {
|
||||
a1 = x * _horner(b1_a1a, y);
|
||||
a2 = _horner(b1_a2a, y);
|
||||
a = a1 / a2 + W * (besselj(x, 1) * Math.log(x) - 1 / x);
|
||||
} else {
|
||||
y = 64 / y;
|
||||
a1 = _horner(b1_a1b, y);
|
||||
a2 = _horner(b1_a2b, y);
|
||||
a = Math.sqrt(W / x) * (Math.sin(xx) * a1 + Math.cos(xx) * a2 * 8 / x);
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
return _bessel_wrap(bessel0, bessel1, 1, -1);
|
||||
})();
|
||||
|
||||
export const besseli = (() => {
|
||||
const b0_a = [0.45813e-2, 0.360768e-1, 0.2659732, 1.2067492, 3.0899424, 3.5156229, 1.0];
|
||||
const b0_b = [0.392377e-2, -0.1647633e-1, 0.2635537e-1, -0.2057706e-1, 0.916281e-2, -0.157565e-2, 0.225319e-2, 0.1328592e-1, 0.39894228];
|
||||
|
||||
function bessel0(x: number): number {
|
||||
if (x <= 3.75) {
|
||||
return _horner(b0_a, x * x / (3.75 * 3.75));
|
||||
}
|
||||
|
||||
return Math.exp(Math.abs(x)) / Math.sqrt(Math.abs(x)) * _horner(b0_b, 3.75 / Math.abs(x));
|
||||
}
|
||||
|
||||
const b1_a = [0.32411e-3, 0.301532e-2, 0.2658733e-1, 0.15084934, 0.51498869, 0.87890594, 0.5];
|
||||
const b1_b = [-0.420059e-2, 0.1787654e-1, -0.2895312e-1, 0.2282967e-1, -0.1031555e-1, 0.163801e-2, -0.362018e-2, -0.3988024e-1, 0.39894228];
|
||||
|
||||
function bessel1(x: number): number {
|
||||
if (x < 3.75) {
|
||||
return x * _horner(b1_a, x * x / (3.75 * 3.75));
|
||||
}
|
||||
|
||||
return (x < 0 ? -1 : 1) * Math.exp(Math.abs(x)) / Math.sqrt(Math.abs(x)) * _horner(b1_b, 3.75 / Math.abs(x));
|
||||
}
|
||||
|
||||
return function besseli(x: number, n: number): number {
|
||||
const _n = Math.round(n);
|
||||
|
||||
if (_n === 0) {
|
||||
return bessel0(x);
|
||||
}
|
||||
|
||||
if (_n === 1) {
|
||||
return bessel1(x);
|
||||
}
|
||||
|
||||
if (_n < 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
if (Math.abs(x) === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x === Infinity) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
let ret = 0.0;
|
||||
let j;
|
||||
const tox = 2 / Math.abs(x);
|
||||
let bip = 0.0;
|
||||
let bi = 1.0;
|
||||
let bim = 0.0;
|
||||
|
||||
const m = 2 * Math.round((_n + Math.round(Math.sqrt(40 * _n))) / 2);
|
||||
|
||||
for (j = m; j > 0; j--) {
|
||||
bim = j * tox * bi + bip;
|
||||
bip = bi;
|
||||
bi = bim;
|
||||
|
||||
if (Math.abs(bi) > 1e10) {
|
||||
bi *= 1e-10;
|
||||
bip *= 1e-10;
|
||||
ret *= 1e-10;
|
||||
}
|
||||
|
||||
if (j === _n) ret = bip;
|
||||
|
||||
// After more than 100 iterations, the result is 0
|
||||
if ((m - j) > 100 && ret === 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
ret *= besseli(x, 0) / bi;
|
||||
|
||||
return x < 0 && (_n % 2) ? -ret : ret;
|
||||
};
|
||||
})();
|
||||
|
||||
export const besselk = (() => {
|
||||
const b0_a = [0.74e-5, 0.10750e-3, 0.262698e-2, 0.3488590e-1, 0.23069756, 0.42278420, -0.57721566];
|
||||
const b0_b = [0.53208e-3, -0.251540e-2, 0.587872e-2, -0.1062446e-1, 0.2189568e-1, -0.7832358e-1, 1.25331414];
|
||||
|
||||
function bessel0(x: number): number {
|
||||
if (x <= 2) {
|
||||
return -Math.log(x / 2) * besseli(x, 0) + _horner(b0_a, x * x / 4);
|
||||
}
|
||||
|
||||
return Math.exp(-x) / Math.sqrt(x) * _horner(b0_b, 2 / x);
|
||||
}
|
||||
|
||||
const b1_a = [-0.4686e-4, -0.110404e-2, -0.1919402e-1, -0.18156897, -0.67278579, 0.15443144, 1.0];
|
||||
const b1_b = [-0.68245e-3, 0.325614e-2, -0.780353e-2, 0.1504268e-1, -0.3655620e-1, 0.23498619, 1.25331414];
|
||||
|
||||
function bessel1(x: number): number {
|
||||
if (x <= 2) {
|
||||
return Math.log(x / 2) * besseli(x, 1) + (1 / x) * _horner(b1_a, x * x / 4);
|
||||
}
|
||||
|
||||
return Math.exp(-x) / Math.sqrt(x) * _horner(b1_b, 2 / x);
|
||||
}
|
||||
|
||||
return _bessel_wrap(bessel0, bessel1, 2, 1);
|
||||
})();
|
||||
}
|
||||
|
||||
export function isValidBinaryNumber(number: string): boolean {
|
||||
return /^[01]{1,10}$/.test(number);
|
||||
}
|
||||
|
||||
export function isValidOctalNumber(number: string): boolean {
|
||||
return /^[0-7]{1,10}$/.test(number);
|
||||
}
|
||||
|
||||
export function isValidHexadecimalNumber(number: string): boolean {
|
||||
return /^[0-9A-Fa-f]{1,10}$/.test(number);
|
||||
}
|
||||
|
||||
export function erf(x: number): number {
|
||||
if (x === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cof = [
|
||||
-1.3026537197817094,
|
||||
// eslint-disable-next-line no-loss-of-precision
|
||||
6.4196979235649026e-1,
|
||||
1.9476473204185836e-2,
|
||||
-9.561514786808631e-3,
|
||||
-9.46595344482036e-4,
|
||||
3.66839497852761e-4,
|
||||
4.2523324806907e-5,
|
||||
-2.0278578112534e-5,
|
||||
-1.624290004647e-6,
|
||||
1.303655835580e-6,
|
||||
1.5626441722e-8,
|
||||
-8.5238095915e-8,
|
||||
6.529054439e-9,
|
||||
5.059343495e-9,
|
||||
-9.91364156e-10,
|
||||
-2.27365122e-10,
|
||||
9.6467911e-11,
|
||||
2.394038e-12,
|
||||
-6.886027e-12,
|
||||
8.94487e-13,
|
||||
3.13092e-13,
|
||||
-1.12708e-13,
|
||||
3.81e-16,
|
||||
7.106e-15,
|
||||
-1.523e-15,
|
||||
-9.4e-17,
|
||||
1.21e-16,
|
||||
-2.8e-17,
|
||||
];
|
||||
|
||||
let _x = x;
|
||||
let isNeg = false;
|
||||
|
||||
if (_x < 0) {
|
||||
_x = -_x;
|
||||
isNeg = true;
|
||||
}
|
||||
|
||||
const t = 2 / (2 + _x);
|
||||
const ty = 4 * t - 2;
|
||||
|
||||
let d = 0;
|
||||
let dd = 0;
|
||||
let tmp;
|
||||
|
||||
for (let j = cof.length - 1; j > 0; j--) {
|
||||
tmp = d;
|
||||
d = ty * d - dd + cof[j];
|
||||
dd = tmp;
|
||||
}
|
||||
|
||||
const res = t * Math.exp(-_x * _x + 0.5 * (cof[0] + ty * d) - dd);
|
||||
|
||||
return isNeg ? res - 1 : 1 - res;
|
||||
};
|
||||
|
||||
export function erfc(x: number): number {
|
||||
return 1 - erf(x);
|
||||
}
|
||||
|
||||
export function erfcINV(p: number): number {
|
||||
if (p >= 2) {
|
||||
return -100;
|
||||
}
|
||||
|
||||
if (p <= 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
const _p = (p < 1) ? p : 2 - p;
|
||||
const temp = Math.sqrt(-2 * Math.log(_p / 2));
|
||||
|
||||
let x = -0.70711 * ((2.30753 + temp * 0.27061) / (1 + temp * (0.99229 + temp * 0.04481)) - temp);
|
||||
|
||||
for (let j = 0; j < 2; j++) {
|
||||
const err = erfc(x) - _p;
|
||||
|
||||
x += err / (1.12837916709551257 * Math.exp(-x * x) - x * err); // eslint-disable-line
|
||||
}
|
||||
|
||||
return (p < 1) ? x : -x;
|
||||
};
|
||||
69
packages/engine-formula/src/basics/error-type.ts
Normal file
69
packages/engine-formula/src/basics/error-type.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum ErrorType {
|
||||
/** Division by zero. */
|
||||
DIV_BY_ZERO = '#DIV/0!',
|
||||
|
||||
/** Function error. */
|
||||
NAME = '#NAME?',
|
||||
VALUE = '#VALUE!',
|
||||
NUM = '#NUM!',
|
||||
NA = '#N/A',
|
||||
|
||||
/** Cyclic dependency. */
|
||||
CYCLE = '#CYCLE!',
|
||||
|
||||
/** Wrong reference. */
|
||||
REF = '#REF!',
|
||||
|
||||
/** Array spill error. */
|
||||
SPILL = '#SPILL!',
|
||||
|
||||
/** Calculation error. */
|
||||
CALC = '#CALC!',
|
||||
|
||||
/** Generic error */
|
||||
ERROR = '#ERROR!',
|
||||
|
||||
/** connected to remote */
|
||||
CONNECT = '#GETTING_DATA',
|
||||
|
||||
/** In the case of SUM(B1 C1), */
|
||||
NULL = '#NULL!',
|
||||
}
|
||||
|
||||
export const ERROR_TYPE_SET = new Set(Object.values(ErrorType));
|
||||
|
||||
export const ERROR_TYPE_COUNT_ARRAY = [...new Set(Object.values(ErrorType).map((error) => error.length))];
|
||||
|
||||
/** Interface for sheet formula errors */
|
||||
export interface ISheetFormulaError {
|
||||
/** sheet name */
|
||||
sheetName: string;
|
||||
|
||||
/** cell row */
|
||||
row: number;
|
||||
|
||||
/** cell column */
|
||||
column: number;
|
||||
|
||||
/** formula string (e.g., "=SUM(A1:B10)") */
|
||||
formula: string;
|
||||
|
||||
/** error type, refer to ErrorType enum */
|
||||
errorType: ErrorType;
|
||||
}
|
||||
691
packages/engine-formula/src/basics/financial.ts
Normal file
691
packages/engine-formula/src/basics/financial.ts
Normal file
@@ -0,0 +1,691 @@
|
||||
/**
|
||||
* 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 { DateSystem, excelDateSerial, excelDateTimePartsToSerial, excelSerialToDate, excelSerialToDateTimeParts } from '@univerjs/core';
|
||||
import { ErrorValueObject } from '../engine/value-object/base-value-object';
|
||||
import {
|
||||
dateAddMonths,
|
||||
getDaysInMonth,
|
||||
getTwoDateDaysByBasis,
|
||||
lastDayOfMonth,
|
||||
} from './date';
|
||||
import { ErrorType } from './error-type';
|
||||
|
||||
interface ICouponDateParts {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
monthEnd: boolean;
|
||||
}
|
||||
|
||||
interface ICouponSchedule {
|
||||
previousSerial: number;
|
||||
nextSerial: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const COUPON_DATE_PARTS_OPTIONS = { hours: 0, minutes: 0, seconds: 0, fractionalSecond: 0 };
|
||||
|
||||
function getCouponDateParts(serial: number, dateSystem: DateSystem): ICouponDateParts {
|
||||
const parts = excelSerialToDateTimeParts(serial, { dateSystem });
|
||||
|
||||
if (parts) {
|
||||
return {
|
||||
year: parts.year,
|
||||
month: parts.month,
|
||||
day: parts.day,
|
||||
monthEnd: parts.day === getDaysInMonth(parts.year, parts.month - 1),
|
||||
};
|
||||
}
|
||||
|
||||
const date = excelSerialToDate(serial, dateSystem);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth() + 1;
|
||||
const day = date.getUTCDate();
|
||||
|
||||
return { year, month, day, monthEnd: day === getDaysInMonth(year, month - 1) };
|
||||
}
|
||||
|
||||
function getCouponDateSerial(parts: ICouponDateParts, dateSystem: DateSystem): number {
|
||||
if (dateSystem === DateSystem.Date1900 && parts.year === 1900 && parts.month === 1 && parts.day === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (dateSystem === DateSystem.Date1900 && parts.year === 1900 && parts.month === 2 && parts.day === 29) {
|
||||
return 60;
|
||||
}
|
||||
|
||||
const serial = excelDateTimePartsToSerial({
|
||||
year: parts.year,
|
||||
month: parts.month,
|
||||
day: parts.day,
|
||||
...COUPON_DATE_PARTS_OPTIONS,
|
||||
}, { dateSystem });
|
||||
|
||||
if (serial != null) {
|
||||
return serial;
|
||||
}
|
||||
|
||||
return excelDateSerial(new Date(Date.UTC(parts.year, parts.month - 1, parts.day)), dateSystem);
|
||||
}
|
||||
|
||||
function compareCouponDate(left: ICouponDateParts, right: ICouponDateParts): number {
|
||||
if (left.year !== right.year) {
|
||||
return left.year - right.year;
|
||||
}
|
||||
|
||||
if (left.month !== right.month) {
|
||||
return left.month - right.month;
|
||||
}
|
||||
|
||||
return left.day - right.day;
|
||||
}
|
||||
|
||||
// Excel keeps the maturity day/month-end rule for every coupon period; it must not be inferred from an intermediate date.
|
||||
function addCouponMonths(parts: ICouponDateParts, months: number): ICouponDateParts {
|
||||
const monthIndex = parts.year * 12 + parts.month - 1 + months;
|
||||
const year = Math.floor(monthIndex / 12);
|
||||
const month = monthIndex - year * 12 + 1;
|
||||
const maxDay = getDaysInMonth(year, month - 1);
|
||||
const day = parts.monthEnd ? maxDay : Math.min(parts.day, maxDay);
|
||||
|
||||
return { year, month, day, monthEnd: parts.monthEnd };
|
||||
}
|
||||
|
||||
function getCouponSchedule(
|
||||
settlementSerialNumber: number,
|
||||
maturitySerialNumber: number,
|
||||
frequency: number,
|
||||
dateSystem: DateSystem
|
||||
): ICouponSchedule {
|
||||
const settlement = getCouponDateParts(settlementSerialNumber, dateSystem);
|
||||
const maturity = getCouponDateParts(maturitySerialNumber, dateSystem);
|
||||
const periodMonths = 12 / frequency;
|
||||
let candidate: ICouponDateParts = {
|
||||
...maturity,
|
||||
year: settlement.year,
|
||||
};
|
||||
|
||||
if (compareCouponDate(candidate, settlement) < 0) {
|
||||
candidate = { ...candidate, year: candidate.year + 1 };
|
||||
}
|
||||
|
||||
while (compareCouponDate(candidate, settlement) > 0) {
|
||||
candidate = addCouponMonths(candidate, -periodMonths);
|
||||
}
|
||||
|
||||
const previousSerial = getCouponDateSerial(candidate, dateSystem);
|
||||
const nextSerial = getCouponDateSerial(addCouponMonths(candidate, periodMonths), dateSystem);
|
||||
|
||||
let count = 0;
|
||||
let countCandidate = maturity;
|
||||
while (compareCouponDate(countCandidate, settlement) > 0) {
|
||||
countCandidate = addCouponMonths(countCandidate, -periodMonths);
|
||||
count++;
|
||||
}
|
||||
|
||||
return { previousSerial, nextSerial, count };
|
||||
}
|
||||
|
||||
export function calculateCoupdaybs(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, basis: number, dateSystem = DateSystem.Date1900): number {
|
||||
const coupDateSerialNumber = calculateCouppcd(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
|
||||
|
||||
const { days } = getTwoDateDaysByBasis(coupDateSerialNumber, settlementSerialNumber, basis, dateSystem);
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
export function calculateCoupdays(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, basis: number, dateSystem = DateSystem.Date1900): number {
|
||||
let result;
|
||||
|
||||
if (basis === 1) {
|
||||
const beforeSettlementDateSerialNumber = calculateCouppcd(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
|
||||
|
||||
const afterSettlementDateSerialNumber = getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).nextSerial;
|
||||
|
||||
// special handle for excel
|
||||
if (beforeSettlementDateSerialNumber < 0 && frequency === 1) {
|
||||
result = 365;
|
||||
} else if (dateSystem === DateSystem.Date1900 && settlementSerialNumber === 0 && frequency === 2 && beforeSettlementDateSerialNumber < 0) {
|
||||
result = 184;
|
||||
} else {
|
||||
result = afterSettlementDateSerialNumber - beforeSettlementDateSerialNumber;
|
||||
}
|
||||
} else if (basis === 3) {
|
||||
result = 365 / frequency;
|
||||
} else {
|
||||
result = 360 / frequency;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function calculateCoupncd(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): number {
|
||||
return getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).nextSerial;
|
||||
}
|
||||
|
||||
export function calculateCoupnum(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): number {
|
||||
return getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).count;
|
||||
}
|
||||
|
||||
export function calculateCouppcd(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): number {
|
||||
return getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).previousSerial;
|
||||
}
|
||||
|
||||
export function calculateDuration(settlementSerialNumber: number, maturitySerialNumber: number, coupon: number, yld: number, frequency: number, basis: number, dateSystem = DateSystem.Date1900): number {
|
||||
const coupdaybs = calculateCoupdaybs(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
|
||||
const coupdays = calculateCoupdays(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
|
||||
const coupnum = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
|
||||
|
||||
const coupdaysDiff = (coupdays - coupdaybs) / coupdays - 1;
|
||||
const _yld = yld / frequency + 1;
|
||||
const _coupon = coupon * 100 / frequency;
|
||||
|
||||
let duration = 0;
|
||||
let den = 0;
|
||||
|
||||
for (let i = 1; i <= coupnum; i++) {
|
||||
const index = i + coupdaysDiff;
|
||||
const num = _coupon / (_yld ** index);
|
||||
|
||||
duration += index * num;
|
||||
den += num;
|
||||
}
|
||||
|
||||
const index = coupnum + coupdaysDiff;
|
||||
const num = 100 / (_yld ** index);
|
||||
|
||||
duration += index * num;
|
||||
den += num;
|
||||
|
||||
return duration / den / frequency;
|
||||
}
|
||||
|
||||
export function calculatePMT(rate: number, nper: number, pv: number, fv: number, type: number): number {
|
||||
// type = 0 Payment at the end of the period
|
||||
// type = 1 Payment at the beginning of the period
|
||||
let result: number;
|
||||
|
||||
if (rate === 0) {
|
||||
result = (pv + fv) / nper;
|
||||
} else {
|
||||
const term = (1 + rate) ** nper;
|
||||
|
||||
result = type === 1
|
||||
? ((fv * rate) / (term - 1) + (pv * rate) / (1 - 1 / term)) / (1 + rate)
|
||||
: (fv * rate) / (term - 1) + (pv * rate) / (1 - 1 / term);
|
||||
}
|
||||
|
||||
return -result;
|
||||
}
|
||||
|
||||
export function calculateFV(rate: number, nper: number, pmt: number, pv: number, type: number): number {
|
||||
// type = 0 Payment at the end of the period
|
||||
// type = 1 Payment at the beginning of the period
|
||||
let result: number;
|
||||
|
||||
if (rate === 0) {
|
||||
result = pv + pmt * nper;
|
||||
} else {
|
||||
if (rate === -1 && nper === 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
const term = (1 + rate) ** nper;
|
||||
|
||||
result = type === 1
|
||||
? pv * term + (pmt * (1 + rate) * (term - 1)) / rate
|
||||
: pv * term + (pmt * (term - 1)) / rate;
|
||||
}
|
||||
|
||||
return -result;
|
||||
}
|
||||
|
||||
export function calculateIPMT(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number {
|
||||
// type = 0 Payment at the end of the period
|
||||
// type = 1 Payment at the beginning of the period
|
||||
const payment = calculatePMT(rate, nper, pv, fv, type);
|
||||
|
||||
const result = per === 1
|
||||
? (type === 1 ? 0 : -pv)
|
||||
: (type === 1 ? calculateFV(rate, per - 2, payment, pv, 1) - payment : calculateFV(rate, per - 1, payment, pv, 0));
|
||||
|
||||
return result * rate;
|
||||
}
|
||||
|
||||
export function calculateNpv(rate: number, values: number[]): number {
|
||||
let res = 0;
|
||||
|
||||
for (let i = 1; i <= values.length; i++) {
|
||||
res += values[i - 1] / ((1 + rate) ** i);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function calculateOddFPrice(
|
||||
settlementSerialNumber: number,
|
||||
maturitySerialNumber: number,
|
||||
issueSerialNumber: number,
|
||||
firstCouponSerialNumber: number,
|
||||
rate: number,
|
||||
yld: number,
|
||||
redemption: number,
|
||||
frequency: number,
|
||||
basis: number,
|
||||
dateSystem = DateSystem.Date1900
|
||||
): number {
|
||||
// DFC = number of days from the beginning of the odd first coupon to the first coupon date.
|
||||
const DFC = getPositiveDaysBetween(issueSerialNumber, firstCouponSerialNumber, basis, dateSystem);
|
||||
|
||||
// E = number of days in the coupon period.
|
||||
const E = calculateCoupdays(settlementSerialNumber, firstCouponSerialNumber, frequency, basis, dateSystem);
|
||||
|
||||
if (DFC < E) {
|
||||
return calculateOddShortFirstCoupon(
|
||||
settlementSerialNumber,
|
||||
maturitySerialNumber,
|
||||
issueSerialNumber,
|
||||
firstCouponSerialNumber,
|
||||
rate,
|
||||
yld,
|
||||
redemption,
|
||||
frequency,
|
||||
basis,
|
||||
DFC,
|
||||
E,
|
||||
dateSystem
|
||||
);
|
||||
} else {
|
||||
return calculateOddLongFirstCoupon(
|
||||
settlementSerialNumber,
|
||||
maturitySerialNumber,
|
||||
issueSerialNumber,
|
||||
firstCouponSerialNumber,
|
||||
rate,
|
||||
yld,
|
||||
redemption,
|
||||
frequency,
|
||||
basis,
|
||||
E,
|
||||
dateSystem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateOddShortFirstCoupon(
|
||||
settlementSerialNumber: number,
|
||||
maturitySerialNumber: number,
|
||||
issueSerialNumber: number,
|
||||
firstCouponSerialNumber: number,
|
||||
rate: number,
|
||||
yld: number,
|
||||
redemption: number,
|
||||
frequency: number,
|
||||
basis: number,
|
||||
DFC: number,
|
||||
E: number,
|
||||
dateSystem: DateSystem
|
||||
): number {
|
||||
// calculate method from
|
||||
// https://support.microsoft.com/en-us/office/oddfprice-function-d7d664a8-34df-4233-8d2b-922bcf6a69e1
|
||||
// Odd short first coupon:
|
||||
let result = 0;
|
||||
|
||||
// N = number of coupons payable between the settlement date and the redemption date. (If this number contains a fraction, it is raised to the next whole number.)
|
||||
const N = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
|
||||
|
||||
// DSC = number of days from the settlement to the next coupon date.
|
||||
const DSC = getPositiveDaysBetween(settlementSerialNumber, firstCouponSerialNumber, basis, dateSystem);
|
||||
|
||||
result += redemption / ((1 + yld / frequency) ** (N - 1 + DSC / E));
|
||||
|
||||
result += (100 * rate / frequency * DFC / E) / ((1 + yld / frequency) ** (DSC / E));
|
||||
|
||||
for (let k = 2; k <= N; k++) {
|
||||
result += (100 * rate / frequency) / ((1 + yld / frequency) ** (k - 1 + DSC / E));
|
||||
}
|
||||
|
||||
// A = number of days from the beginning of the coupon period to the settlement date (accrued days).
|
||||
const A = getPositiveDaysBetween(issueSerialNumber, settlementSerialNumber, basis, dateSystem);
|
||||
|
||||
result -= 100 * rate / frequency * A / E;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function calculateOddLongFirstCoupon(
|
||||
settlementSerialNumber: number,
|
||||
maturitySerialNumber: number,
|
||||
issueSerialNumber: number,
|
||||
firstCouponSerialNumber: number,
|
||||
rate: number,
|
||||
yld: number,
|
||||
redemption: number,
|
||||
frequency: number,
|
||||
basis: number,
|
||||
E: number,
|
||||
dateSystem: DateSystem
|
||||
): number {
|
||||
// calculate method from
|
||||
// https://support.microsoft.com/en-us/office/oddfprice-function-d7d664a8-34df-4233-8d2b-922bcf6a69e1
|
||||
// Odd long first coupon:
|
||||
let result = 0;
|
||||
|
||||
// N = number of coupons payable between the first real coupon date and redemption date. (If this number contains a fraction, it is raised to the next whole number.)
|
||||
const N = calculateCoupnum(firstCouponSerialNumber, maturitySerialNumber, frequency, dateSystem);
|
||||
|
||||
// Nq = number of whole quasi-coupon periods between settlement date and first coupon.
|
||||
const Nq = getCouponsNumber(firstCouponSerialNumber, settlementSerialNumber, 12 / frequency, true, dateSystem);
|
||||
|
||||
// DSC = number of days from the settlement to the next coupon date.
|
||||
let DSC;
|
||||
|
||||
if (basis === 2 || basis === 3) {
|
||||
const coupncd = calculateCoupncd(settlementSerialNumber, firstCouponSerialNumber, frequency, dateSystem);
|
||||
DSC = getPositiveDaysBetween(settlementSerialNumber, coupncd, basis, dateSystem);
|
||||
} else {
|
||||
const couppcd = calculateCouppcd(settlementSerialNumber, firstCouponSerialNumber, frequency, dateSystem);
|
||||
const { days } = getTwoDateDaysByBasis(couppcd, settlementSerialNumber, basis, dateSystem);
|
||||
DSC = E - days;
|
||||
}
|
||||
|
||||
result += redemption / ((1 + yld / frequency) ** (N + Nq + DSC / E));
|
||||
|
||||
// NC = number of quasi-coupon periods that fit in odd period. (If this number contains a fraction, it is raised to the next whole number.)
|
||||
const NC = calculateCoupnum(issueSerialNumber, firstCouponSerialNumber, frequency, dateSystem);
|
||||
|
||||
let lateCoupon = firstCouponSerialNumber;
|
||||
let DCiDivNLiSum = 0;
|
||||
let AiDivNLiSum = 0;
|
||||
|
||||
for (let index = NC; index >= 1; index--) {
|
||||
const earlyCoupon = getDateSerialNumberByMonths(lateCoupon, -12 / frequency, false, dateSystem);
|
||||
const NLi = basis === 1 ? getPositiveDaysBetween(earlyCoupon, lateCoupon, basis, dateSystem) : E;
|
||||
const DCi = index > 1 ? NLi : getPositiveDaysBetween(issueSerialNumber, lateCoupon, basis, dateSystem);
|
||||
|
||||
DCiDivNLiSum += DCi / NLi;
|
||||
|
||||
const startDate = issueSerialNumber > earlyCoupon ? issueSerialNumber : earlyCoupon;
|
||||
const endDate = settlementSerialNumber < lateCoupon ? settlementSerialNumber : lateCoupon;
|
||||
const Ai = getPositiveDaysBetween(startDate, endDate, basis, dateSystem);
|
||||
|
||||
AiDivNLiSum += Ai / NLi;
|
||||
|
||||
lateCoupon = earlyCoupon;
|
||||
}
|
||||
|
||||
result += (100 * rate / frequency * DCiDivNLiSum) / ((1 + yld / frequency) ** (Nq + DSC / E));
|
||||
|
||||
for (let k = 1; k <= N; k++) {
|
||||
result += (100 * rate / frequency) / ((1 + yld / frequency) ** (k + Nq + DSC / E));
|
||||
}
|
||||
|
||||
result -= 100 * rate / frequency * AiDivNLiSum;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getPositiveDaysBetween(startDateSerialNumber: number, endDateSerialNumber: number, basis: number, dateSystem: DateSystem): number {
|
||||
const { days } = getTwoDateDaysByBasis(startDateSerialNumber, endDateSerialNumber, basis, dateSystem);
|
||||
|
||||
return startDateSerialNumber < endDateSerialNumber ? days : 0;
|
||||
}
|
||||
|
||||
export function validDaysBetweenIsWholeFrequencyByTwoDate(date1SerialNumber: number, date2SerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): boolean {
|
||||
const date1 = excelSerialToDate(date1SerialNumber, dateSystem);
|
||||
const date1Year = date1.getUTCFullYear();
|
||||
const date1Month = date1.getUTCMonth();
|
||||
const date1Day = date1.getUTCDate();
|
||||
const date1LastDayOfMonth = lastDayOfMonth(date1Year, date1Month, date1Day);
|
||||
|
||||
const date2 = excelSerialToDate(date2SerialNumber, dateSystem);
|
||||
const date2Year = date2.getUTCFullYear();
|
||||
const date2Month = date2.getUTCMonth();
|
||||
const date2Day = date2.getUTCDate();
|
||||
const date2LastDayOfMonth = lastDayOfMonth(date2Year, date2Month, date2Day);
|
||||
|
||||
if (date1Day !== date2Day && !(date1LastDayOfMonth && date2LastDayOfMonth)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const months = Math.abs((date2Year - date1Year) * 12 + (date2Month - date1Month));
|
||||
|
||||
if (months % (12 / frequency) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validCouppcdIsGte0ByTwoDate(date1SerialNumber: number, date2SerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): boolean {
|
||||
const couppcd = calculateCouppcd(date1SerialNumber, date2SerialNumber, frequency, dateSystem);
|
||||
return couppcd >= 0;
|
||||
}
|
||||
|
||||
export function getDateSerialNumberByMonths(serialNumber: number, months: number, returnLastDay: boolean, dateSystem = DateSystem.Date1900): number {
|
||||
let date = excelSerialToDate(serialNumber, dateSystem);
|
||||
date = dateAddMonths(date, months);
|
||||
|
||||
if (returnLastDay) {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth();
|
||||
const daysInMonth = getDaysInMonth(year, month);
|
||||
date.setUTCDate(daysInMonth);
|
||||
}
|
||||
|
||||
return excelDateSerial(date, dateSystem);
|
||||
}
|
||||
|
||||
function getCouponsNumber(startDateSerialNumber: number, endDateSerialNumber: number, months: number, isWholeNumber: boolean, dateSystem: DateSystem): number {
|
||||
const startDate = excelSerialToDate(startDateSerialNumber, dateSystem);
|
||||
const endDate = excelSerialToDate(endDateSerialNumber, dateSystem);
|
||||
|
||||
const startDateYear = startDate.getUTCFullYear();
|
||||
const startDateMonth = startDate.getUTCMonth();
|
||||
const startDateDay = startDate.getUTCDate();
|
||||
|
||||
const endDateYear = endDate.getUTCFullYear();
|
||||
const endDateMonth = endDate.getUTCMonth();
|
||||
const endDateDay = endDate.getUTCDate();
|
||||
|
||||
const endOfMonthTemp = lastDayOfMonth(startDateYear, startDateMonth, startDateDay);
|
||||
const endOfMonth = (!endOfMonthTemp && startDateMonth !== 1 && startDateDay > 28 && startDateDay < getDaysInMonth(startDateYear, startDateMonth))
|
||||
? lastDayOfMonth(endDateYear, endDateMonth, endDateDay)
|
||||
: endOfMonthTemp;
|
||||
|
||||
const newDateSerialNumber = getDateSerialNumberByMonths(endDateSerialNumber, 0, endOfMonth, dateSystem);
|
||||
|
||||
let coupons = (+isWholeNumber - 0) + +(endDateSerialNumber < newDateSerialNumber);
|
||||
let frontDateSerialNumber = getDateSerialNumberByMonths(newDateSerialNumber, months, endOfMonth, dateSystem);
|
||||
|
||||
while (!(months > 0 ? frontDateSerialNumber >= endDateSerialNumber : frontDateSerialNumber <= endDateSerialNumber)) {
|
||||
frontDateSerialNumber = getDateSerialNumberByMonths(frontDateSerialNumber, months, endOfMonth, dateSystem);
|
||||
coupons++;
|
||||
}
|
||||
|
||||
return coupons;
|
||||
}
|
||||
|
||||
interface IIterFFunctionType {
|
||||
(x: number): number;
|
||||
}
|
||||
|
||||
export function getResultByGuessIterF(guess: number, iterF: IIterFFunctionType): number | ErrorValueObject {
|
||||
const g_Eps = 1e-7;
|
||||
const g_Eps2 = g_Eps * 2;
|
||||
const nIM = 500;
|
||||
|
||||
let eps = 1;
|
||||
let nMC = 0;
|
||||
let x = guess;
|
||||
let xN;
|
||||
|
||||
while (eps > g_Eps && nMC < nIM) {
|
||||
const den = (iterF(x + g_Eps) - iterF(x - g_Eps)) / g_Eps2;
|
||||
xN = x - iterF(x) / den;
|
||||
nMC++;
|
||||
eps = Math.abs(xN - x);
|
||||
x = xN;
|
||||
}
|
||||
|
||||
if (Number.isNaN(x) || Infinity === Math.abs(x) || nMC === nIM) {
|
||||
return guessIsNaNorInfinity(guess, iterF);
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
function guessIsNaNorInfinity(guess: number, iterF: IIterFFunctionType): number | ErrorValueObject {
|
||||
const g_Eps = 1e-7;
|
||||
const nIM = 60;
|
||||
|
||||
const max = Number.MAX_VALUE;
|
||||
const min = -1;
|
||||
const step = 1.6;
|
||||
|
||||
let low = guess - 0.01 <= min ? min + g_Eps : guess - 0.01;
|
||||
let high = guess + 0.01 >= max ? max - g_Eps : guess + 0.01;
|
||||
let xBegin;
|
||||
let xEnd;
|
||||
let currentIter = 0;
|
||||
|
||||
if (guess <= min || guess >= max) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
for (let i = 0; i < nIM; i++) {
|
||||
xBegin = low <= min ? min + g_Eps : low;
|
||||
xEnd = high >= max ? max - g_Eps : high;
|
||||
|
||||
const x = iterF(xBegin);
|
||||
const y = iterF(xEnd);
|
||||
|
||||
if (x * y <= 0) {
|
||||
break;
|
||||
} else if (x * y > 0) {
|
||||
low = (xBegin + step * (xBegin - xEnd));
|
||||
high = (xEnd + step * (xEnd - xBegin));
|
||||
} else {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
if (i === nIM - 1) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
}
|
||||
|
||||
xBegin = xBegin as number;
|
||||
xEnd = xEnd as number;
|
||||
|
||||
let fXbegin = iterF(xBegin);
|
||||
const fXend = iterF(xEnd);
|
||||
let fXi;
|
||||
let xI;
|
||||
|
||||
if (Math.abs(fXbegin) < g_Eps) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
if (Math.abs(fXend) < g_Eps) {
|
||||
return ErrorValueObject.create(ErrorType.NUM);
|
||||
}
|
||||
|
||||
do {
|
||||
xI = xBegin + (xEnd - xBegin) / 2;
|
||||
fXi = iterF(xI);
|
||||
|
||||
if (fXbegin * fXi < 0) {
|
||||
xEnd = xI;
|
||||
} else {
|
||||
xBegin = xI;
|
||||
}
|
||||
|
||||
fXbegin = iterF(xBegin);
|
||||
currentIter++;
|
||||
} while (Math.abs(fXi) > g_Eps && currentIter < nIM);
|
||||
|
||||
return xI;
|
||||
}
|
||||
|
||||
export function calculatePrice(
|
||||
settlementSerialNumber: number,
|
||||
maturitySerialNumber: number,
|
||||
rate: number,
|
||||
yld: number,
|
||||
redemption: number,
|
||||
frequency: number,
|
||||
basis: number,
|
||||
dateSystem = DateSystem.Date1900
|
||||
): number {
|
||||
// N is the number of coupons payable between the settlement date and redemption date
|
||||
const N = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
|
||||
|
||||
// E = number of days in coupon period in which the settlement date falls.
|
||||
const E = calculateCoupdays(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
|
||||
|
||||
// A = number of days from beginning of coupon period to settlement date.
|
||||
const A = calculateCoupdaybs(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
|
||||
|
||||
if (N === 1) {
|
||||
const DSR = E - A;
|
||||
const T1 = 100 * rate / frequency + redemption;
|
||||
const T2 = yld / frequency * DSR / E + 1;
|
||||
const T3 = 100 * rate / frequency * A / E;
|
||||
|
||||
return T1 / T2 - T3;
|
||||
}
|
||||
|
||||
const DSC = E - A;
|
||||
|
||||
let result = redemption / ((1 + yld / frequency) ** (N - 1 + DSC / E));
|
||||
|
||||
for (let k = 1; k <= N; k++) {
|
||||
result += (100 * rate / frequency) / ((1 + yld / frequency) ** (k - 1 + DSC / E));
|
||||
}
|
||||
|
||||
result -= 100 * rate / frequency * A / E;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function calculateDDB(cost: number, salvage: number, life: number, period: number, factor: number): number {
|
||||
let oldCost = 0;
|
||||
let fdl = factor / life;
|
||||
|
||||
if (fdl >= 1) {
|
||||
fdl = 1;
|
||||
oldCost = period === 1 ? cost : 0;
|
||||
} else {
|
||||
oldCost = cost * ((1 - fdl) ** (period - 1));
|
||||
}
|
||||
|
||||
const newCost = cost * ((1 - fdl) ** period);
|
||||
|
||||
let result = 0;
|
||||
|
||||
if (newCost < salvage) {
|
||||
result = oldCost - salvage;
|
||||
} else {
|
||||
result = oldCost - newCost;
|
||||
}
|
||||
|
||||
if (result < 0) {
|
||||
result = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
54
packages/engine-formula/src/basics/format.ts
Normal file
54
packages/engine-formula/src/basics/format.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { DateSystem } from '@univerjs/core';
|
||||
import type { BaseValueObject } from '../engine/value-object/base-value-object';
|
||||
import { numfmt } from '@univerjs/core';
|
||||
import { stripErrorMargin } from '../engine/utils/math-kit';
|
||||
|
||||
/**
|
||||
* Convert a number to a preview string using the specified pattern.
|
||||
*
|
||||
* @param pattern Number format pattern.
|
||||
* @param value Number to format.
|
||||
* @returns Formatted preview string.
|
||||
*/
|
||||
export const getFormatPreview = (pattern: string, value: number, dateSystem?: DateSystem) => {
|
||||
return numfmt.format(pattern, value, { dateSystem, throws: false });
|
||||
};
|
||||
|
||||
export const getTextValueOfNumberFormat = (text: BaseValueObject): string => {
|
||||
let textValue = `${text.getValue()}`;
|
||||
|
||||
if (text.isNull()) {
|
||||
textValue = '';
|
||||
}
|
||||
|
||||
if (text.isBoolean()) {
|
||||
textValue = textValue.toLocaleUpperCase();
|
||||
}
|
||||
|
||||
if (text.isNumber()) {
|
||||
if (text.getPattern() !== '') {
|
||||
textValue = getFormatPreview(text.getPattern(), +text.getValue(), text.getDateSystem());
|
||||
} else {
|
||||
// Specify Number.EPSILON to not discard necessary digits in the case of non-precision errors, for example, the length of 1/3 is 17
|
||||
textValue = `${stripErrorMargin(+text.getValue())}`;
|
||||
}
|
||||
}
|
||||
|
||||
return textValue;
|
||||
};
|
||||
212
packages/engine-formula/src/basics/function.ts
Normal file
212
packages/engine-formula/src/basics/function.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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 { BooleanNumber } from '@univerjs/core';
|
||||
import type { FUNCTION_NAMES_ARRAY } from '../functions/array/function-names';
|
||||
import type { FUNCTION_NAMES_COMPATIBILITY } from '../functions/compatibility/function-names';
|
||||
import type { FUNCTION_NAMES_CUBE } from '../functions/cube/function-names';
|
||||
import type { FUNCTION_NAMES_DATABASE } from '../functions/database/function-names';
|
||||
import type { FUNCTION_NAMES_DATE } from '../functions/date/function-names';
|
||||
import type { FUNCTION_NAMES_ENGINEERING } from '../functions/engineering/function-names';
|
||||
import type { FUNCTION_NAMES_FINANCIAL } from '../functions/financial/function-names';
|
||||
import type { FUNCTION_NAMES_INFORMATION } from '../functions/information/function-names';
|
||||
import type { FUNCTION_NAMES_LOGICAL } from '../functions/logical/function-names';
|
||||
import type { FUNCTION_NAMES_LOOKUP } from '../functions/lookup/function-names';
|
||||
import type { FUNCTION_NAMES_MATH } from '../functions/math/function-names';
|
||||
import type { FUNCTION_NAMES_META } from '../functions/meta/function-names';
|
||||
import type { FUNCTION_NAMES_STATISTICAL } from '../functions/statistical/function-names';
|
||||
import type { FUNCTION_NAMES_TEXT } from '../functions/text/function-names';
|
||||
import type { FUNCTION_NAMES_UNIVER } from '../functions/univer/function-names';
|
||||
import type { FUNCTION_NAMES_WEB } from '../functions/web/function-names';
|
||||
|
||||
/**
|
||||
* Function type, refer to https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb
|
||||
*/
|
||||
export enum FunctionType {
|
||||
/**
|
||||
* Financial Functions
|
||||
*/
|
||||
|
||||
Financial,
|
||||
/**
|
||||
* Date and Time Functions
|
||||
*/
|
||||
Date,
|
||||
|
||||
/**
|
||||
* Math and Trigonometry Functions
|
||||
*/
|
||||
Math,
|
||||
|
||||
/**
|
||||
* Statistical Functions
|
||||
*/
|
||||
Statistical,
|
||||
|
||||
/**
|
||||
* Lookup and Reference Functions
|
||||
*/
|
||||
Lookup,
|
||||
|
||||
/**
|
||||
* Database Functions
|
||||
*/
|
||||
Database,
|
||||
|
||||
/**
|
||||
* Text Functions
|
||||
*/
|
||||
Text,
|
||||
|
||||
/**
|
||||
* Logical Functions
|
||||
*/
|
||||
Logical,
|
||||
|
||||
/**
|
||||
* Information Functions
|
||||
*/
|
||||
Information,
|
||||
|
||||
/**
|
||||
* Engineering Functions
|
||||
*/
|
||||
Engineering,
|
||||
|
||||
/**
|
||||
* Cube Functions
|
||||
*/
|
||||
Cube,
|
||||
|
||||
/**
|
||||
* Compatibility Functions
|
||||
*/
|
||||
Compatibility,
|
||||
|
||||
/**
|
||||
* Web Functions
|
||||
*/
|
||||
Web,
|
||||
|
||||
/**
|
||||
* Array Functions
|
||||
*/
|
||||
Array,
|
||||
|
||||
/**
|
||||
* Univer-specific functions
|
||||
*/
|
||||
Univer,
|
||||
|
||||
/**
|
||||
* User-defined functions
|
||||
*/
|
||||
User,
|
||||
|
||||
/**
|
||||
* Defined name
|
||||
*/
|
||||
DefinedName,
|
||||
|
||||
/**
|
||||
* Super Table name
|
||||
*/
|
||||
Table,
|
||||
}
|
||||
|
||||
export interface IFunctionParam {
|
||||
/**
|
||||
* Function name, with internationalization
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Detailed description of function, with internationalization
|
||||
*/
|
||||
detail: string;
|
||||
|
||||
/**
|
||||
* Function example
|
||||
*/
|
||||
example: string;
|
||||
|
||||
/**
|
||||
* Is it optional
|
||||
*
|
||||
* true: required
|
||||
* false: optional
|
||||
*/
|
||||
require: BooleanNumber;
|
||||
|
||||
/**
|
||||
* Whether it is repeatable, in the case of repeatability, the maximum parameter of m is generally set to 255, such as [1,255]
|
||||
*
|
||||
* true: repeatable
|
||||
* false: not repeatable
|
||||
*/
|
||||
repeat: BooleanNumber;
|
||||
}
|
||||
|
||||
export interface IFunctionInfo {
|
||||
/**
|
||||
* Function name
|
||||
*/
|
||||
functionName: string;
|
||||
|
||||
/**
|
||||
* Alias function name
|
||||
*/
|
||||
aliasFunctionName?: string;
|
||||
|
||||
/**
|
||||
* Function type
|
||||
*/
|
||||
functionType: FunctionType;
|
||||
|
||||
/**
|
||||
* Detailed description
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Concise abstract
|
||||
*/
|
||||
abstract: string;
|
||||
|
||||
/**
|
||||
* Function params
|
||||
*/
|
||||
functionParameter: IFunctionParam[];
|
||||
}
|
||||
|
||||
export type IFunctionNames =
|
||||
| string
|
||||
| FUNCTION_NAMES_ARRAY
|
||||
| FUNCTION_NAMES_COMPATIBILITY
|
||||
| FUNCTION_NAMES_CUBE
|
||||
| FUNCTION_NAMES_DATABASE
|
||||
| FUNCTION_NAMES_DATE
|
||||
| FUNCTION_NAMES_ENGINEERING
|
||||
| FUNCTION_NAMES_FINANCIAL
|
||||
| FUNCTION_NAMES_INFORMATION
|
||||
| FUNCTION_NAMES_LOGICAL
|
||||
| FUNCTION_NAMES_LOOKUP
|
||||
| FUNCTION_NAMES_MATH
|
||||
| FUNCTION_NAMES_META
|
||||
| FUNCTION_NAMES_STATISTICAL
|
||||
| FUNCTION_NAMES_TEXT
|
||||
| FUNCTION_NAMES_UNIVER
|
||||
| FUNCTION_NAMES_WEB;
|
||||
307
packages/engine-formula/src/basics/inverted-index-cache.ts
Normal file
307
packages/engine-formula/src/basics/inverted-index-cache.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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 { NumericTuple } from '@flatten-js/interval-tree';
|
||||
import type { ErrorType } from './error-type';
|
||||
import IntervalTree from '@flatten-js/interval-tree';
|
||||
import { isRealNum } from '@univerjs/core';
|
||||
import { ERROR_TYPE_SET } from './error-type';
|
||||
|
||||
type ValueType = string | number | boolean;
|
||||
type ValueTypeWithNullUndefined = ValueType | null | undefined;
|
||||
type ValueTypeWithSymbol = ValueType | symbol;
|
||||
|
||||
// empty cell, empty string and null value share the same inverted index cache key
|
||||
export const DEFAULT_EMPTY_CELL_KEY = Symbol('EMPTY_CELL');
|
||||
|
||||
const normalizedValueMap: Map<ValueTypeWithNullUndefined, ValueTypeWithSymbol> = new Map();
|
||||
|
||||
function normalizeValue(value: ValueTypeWithNullUndefined): ValueTypeWithSymbol {
|
||||
if (normalizedValueMap.has(value)) {
|
||||
return normalizedValueMap.get(value) as ValueTypeWithSymbol;
|
||||
}
|
||||
|
||||
let _value: ValueTypeWithSymbol;
|
||||
|
||||
if (value === null || value === undefined || value === '') {
|
||||
_value = DEFAULT_EMPTY_CELL_KEY;
|
||||
} else if (isRealNum(value) && Number(value).toString() === value.toString()) {
|
||||
// Number string that can be converted to number will be converted to number and stored in the inverted index cache, but '12a' or '012' will not be converted to number, because they are not pure number string.
|
||||
_value = Number(value) === 0 ? 0 : Number(value);
|
||||
} else if (typeof value === 'string') {
|
||||
// For string value, we will convert it to lower case to make the compare operation case-insensitive.
|
||||
_value = value.toLowerCase();
|
||||
} else {
|
||||
_value = value;
|
||||
}
|
||||
|
||||
normalizedValueMap.set(value, _value);
|
||||
return _value;
|
||||
}
|
||||
|
||||
export class InvertedIndexCache {
|
||||
/**
|
||||
* {
|
||||
* unitId:{
|
||||
* sheetId:{
|
||||
* 'columnIndex': {
|
||||
* 10:[1,3,4,5],
|
||||
* 5:[2,6,11,22]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
private _cache: Map<string, Map<string, Map<number, Map<ValueTypeWithSymbol, Set<number>>>>> = new Map();
|
||||
|
||||
private _continueBuildingCache: Map<string, Map<string, Map<number, IntervalTree<NumericTuple>>>> = new Map();
|
||||
|
||||
set(unitId: string, sheetId: string, column: number, value: ValueTypeWithNullUndefined, row: number, isForceUpdate: boolean = false) {
|
||||
if (!this.shouldContinueBuildingCache(unitId, sheetId, column, row) && !isForceUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unitMap = this._cache.get(unitId);
|
||||
if (unitMap == null) {
|
||||
unitMap = new Map();
|
||||
this._cache.set(unitId, unitMap);
|
||||
}
|
||||
|
||||
let sheetMap = unitMap.get(sheetId);
|
||||
if (sheetMap == null) {
|
||||
sheetMap = new Map();
|
||||
unitMap.set(sheetId, sheetMap);
|
||||
}
|
||||
|
||||
let columnMap = sheetMap.get(column);
|
||||
if (columnMap == null) {
|
||||
columnMap = new Map();
|
||||
sheetMap.set(column, columnMap);
|
||||
}
|
||||
|
||||
// If is force update, we need to remove the old value from the map
|
||||
if (isForceUpdate) {
|
||||
for (const [_, _cellList] of columnMap) {
|
||||
if (_cellList.has(row)) {
|
||||
_cellList.delete(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _value = normalizeValue(value);
|
||||
|
||||
let cellList = columnMap.get(_value);
|
||||
if (cellList == null) {
|
||||
cellList = new Set<number>();
|
||||
columnMap.set(_value, cellList);
|
||||
}
|
||||
|
||||
cellList.add(row);
|
||||
}
|
||||
|
||||
getCellValuePositions(unitId: string, sheetId: string, column: number) {
|
||||
return this._cache.get(unitId)?.get(sheetId)?.get(column);
|
||||
}
|
||||
|
||||
getCellPositions(unitId: string, sheetId: string, column: number, value: ValueTypeWithNullUndefined, rowsInCache: NumericTuple[]): {
|
||||
errorType: ErrorType | null;
|
||||
matchingRows: number[];
|
||||
} | undefined {
|
||||
const columnMap = this._cache.get(unitId)?.get(sheetId)?.get(column);
|
||||
if (!columnMap) return;
|
||||
|
||||
const result: {
|
||||
errorType: ErrorType | null;
|
||||
matchingRows: number[];
|
||||
} = {
|
||||
errorType: null,
|
||||
matchingRows: [],
|
||||
};
|
||||
|
||||
const _value = normalizeValue(value);
|
||||
|
||||
if (ERROR_TYPE_SET.has(_value as ErrorType)) {
|
||||
result.errorType = _value as ErrorType;
|
||||
} else if (_value === 0) {
|
||||
const rows = [];
|
||||
|
||||
const rowsForZero = columnMap.get(0);
|
||||
if (rowsForZero) {
|
||||
rows.push(...rowsForZero);
|
||||
}
|
||||
|
||||
const rowsForEmpty = columnMap.get(DEFAULT_EMPTY_CELL_KEY);
|
||||
if (rowsForEmpty) {
|
||||
rows.push(...rowsForEmpty);
|
||||
}
|
||||
|
||||
result.matchingRows = rows.filter((row) => rowsInCache.some(([start, end]) => row >= start && row <= end));
|
||||
} else if (_value === DEFAULT_EMPTY_CELL_KEY) {
|
||||
result.matchingRows = Array.from(columnMap.get(DEFAULT_EMPTY_CELL_KEY) ?? []).filter((row) => rowsInCache.some(([start, end]) => row >= start && row <= end));
|
||||
} else {
|
||||
result.matchingRows = Array.from(columnMap.get(_value) ?? []).filter((row) => rowsInCache.some(([start, end]) => row >= start && row <= end));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
setContinueBuildingCache(unitId: string, sheetId: string, column: number, startRow: number, endRow: number) {
|
||||
if (column === -1 || startRow === -1 || endRow === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unitMap = this._continueBuildingCache.get(unitId);
|
||||
if (unitMap == null) {
|
||||
unitMap = new Map();
|
||||
this._continueBuildingCache.set(unitId, unitMap);
|
||||
}
|
||||
|
||||
let sheetMap = unitMap.get(sheetId);
|
||||
if (sheetMap == null) {
|
||||
sheetMap = new Map();
|
||||
unitMap.set(sheetId, sheetMap);
|
||||
}
|
||||
|
||||
let columnMap = sheetMap.get(column);
|
||||
if (columnMap == null) {
|
||||
columnMap = new IntervalTree<NumericTuple>();
|
||||
columnMap.insert([startRow, endRow]);
|
||||
sheetMap.set(column, columnMap);
|
||||
return;
|
||||
}
|
||||
|
||||
this._handleNewInterval(columnMap, startRow, endRow);
|
||||
}
|
||||
|
||||
shouldContinueBuildingCache(unitId: string, sheetId: string, column: number, row: number) {
|
||||
if (column === -1 || row === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const columnMap = this._continueBuildingCache.get(unitId)?.get(sheetId)?.get(column);
|
||||
|
||||
if (!columnMap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = columnMap.search([row, row]);
|
||||
|
||||
return result.length === 0;
|
||||
}
|
||||
|
||||
canUseCache(unitId: string, sheetId: string, column: number, rangeStartRow: number, rangeEndRow: number) {
|
||||
const columnMap = this._continueBuildingCache.get(unitId)?.get(sheetId)?.get(column);
|
||||
|
||||
if (column === -1 || rangeStartRow === -1 || rangeEndRow === -1 || !columnMap) {
|
||||
return {
|
||||
rowsInCache: [],
|
||||
rowsNotInCache: [],
|
||||
};
|
||||
}
|
||||
|
||||
const result = columnMap.search([rangeStartRow, rangeEndRow]);
|
||||
|
||||
if (result.length === 0) {
|
||||
return {
|
||||
rowsInCache: [],
|
||||
rowsNotInCache: [],
|
||||
};
|
||||
}
|
||||
|
||||
result.sort((a, b) => a[0] - b[0]);
|
||||
|
||||
const rowsInCache: NumericTuple[] = [];
|
||||
const rowsNotInCache: NumericTuple[] = [];
|
||||
|
||||
let _rangeStartRow = rangeStartRow;
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const [start, end] = result[i];
|
||||
|
||||
if (_rangeStartRow >= start) {
|
||||
if (rangeEndRow <= end) {
|
||||
rowsInCache.push([_rangeStartRow, rangeEndRow]);
|
||||
break;
|
||||
}
|
||||
|
||||
rowsInCache.push([_rangeStartRow, end]);
|
||||
_rangeStartRow = end + 1;
|
||||
|
||||
if (i === result.length - 1 && _rangeStartRow <= rangeEndRow) {
|
||||
rowsNotInCache.push([_rangeStartRow, rangeEndRow]);
|
||||
}
|
||||
} else {
|
||||
if (rangeEndRow > end) {
|
||||
rowsInCache.push([start, end]);
|
||||
rowsNotInCache.push([_rangeStartRow, start - 1]);
|
||||
_rangeStartRow = end + 1;
|
||||
|
||||
if (i === result.length - 1 && _rangeStartRow <= rangeEndRow) {
|
||||
rowsNotInCache.push([_rangeStartRow, rangeEndRow]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
rowsInCache.push([start, rangeEndRow]);
|
||||
rowsNotInCache.push([_rangeStartRow, start - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rowsInCache,
|
||||
rowsNotInCache,
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._cache.clear();
|
||||
this._continueBuildingCache.clear();
|
||||
normalizedValueMap.clear();
|
||||
}
|
||||
|
||||
private _handleNewInterval(columnMap: IntervalTree<NumericTuple>, startRow: number, endRow: number) {
|
||||
let result = columnMap.search([startRow, endRow]);
|
||||
|
||||
// the range is not overlapping with any existing range
|
||||
if (result.length === 0) {
|
||||
// check if the range is adjacent to any existing range
|
||||
const adjacentRange: NumericTuple = [startRow - 1 < 0 ? 0 : startRow - 1, endRow + 1];
|
||||
|
||||
result = columnMap.search(adjacentRange);
|
||||
|
||||
// the range is not overlapping or adjacent to any existing range, then insert it
|
||||
if (result.length === 0) {
|
||||
columnMap.insert([startRow, endRow]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// merge overlapping or adjacent ranges
|
||||
let min = startRow;
|
||||
let max = endRow;
|
||||
|
||||
for (const interval of result) {
|
||||
min = Math.min(min, interval[0]);
|
||||
max = Math.max(max, interval[1]);
|
||||
columnMap.remove(interval);
|
||||
}
|
||||
|
||||
columnMap.insert([min, max]);
|
||||
}
|
||||
}
|
||||
|
||||
export const CELL_INVERTED_INDEX_CACHE = new InvertedIndexCache();
|
||||
113
packages/engine-formula/src/basics/match-token.ts
Normal file
113
packages/engine-formula/src/basics/match-token.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 { compareToken, matchToken, operatorToken, prefixToken, suffixToken } from './token';
|
||||
|
||||
export const FORMULA_LEXER_TOKENS = [
|
||||
...Object.values(compareToken),
|
||||
...Object.values(operatorToken),
|
||||
...Object.values(matchToken),
|
||||
...Object.values(suffixToken),
|
||||
...Object.values(prefixToken),
|
||||
];
|
||||
|
||||
export function isFormulaLexerToken(str: string) {
|
||||
return FORMULA_LEXER_TOKENS.includes(str as compareToken);
|
||||
}
|
||||
|
||||
export function includeFormulaLexerToken(str: string) {
|
||||
for (const token of FORMULA_LEXER_TOKENS) {
|
||||
if (str.indexOf(token) > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function normalizeSheetName(sheetName: string) {
|
||||
if (sheetName[0] === "'" && sheetName[sheetName.length - 1] === "'") {
|
||||
return sheetName.substring(1, sheetName.length - 1);
|
||||
}
|
||||
return sheetName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the character is a token keyword for the formula engine.
|
||||
* @param char
|
||||
*/
|
||||
export function matchRefDrawToken(char: string) {
|
||||
return (
|
||||
(isFormulaLexerToken(char) &&
|
||||
char !== matchToken.CLOSE_BRACES &&
|
||||
char !== matchToken.CLOSE_BRACKET &&
|
||||
char !== matchToken.SINGLE_QUOTATION &&
|
||||
char !== matchToken.DOUBLE_QUOTATION) ||
|
||||
char === ' '
|
||||
);
|
||||
}
|
||||
|
||||
export const TOKEN_CANNOT_BE_AT_END_SET = new Set<string>([
|
||||
operatorToken.PLUS,
|
||||
operatorToken.MINUS,
|
||||
operatorToken.MULTIPLY,
|
||||
operatorToken.DIVIDED,
|
||||
operatorToken.CONCATENATE,
|
||||
operatorToken.POWER,
|
||||
operatorToken.EQUALS,
|
||||
operatorToken.NOT_EQUAL,
|
||||
operatorToken.GREATER_THAN,
|
||||
operatorToken.GREATER_THAN_OR_EQUAL,
|
||||
operatorToken.LESS_THAN,
|
||||
operatorToken.LESS_THAN_OR_EQUAL,
|
||||
matchToken.OPEN_BRACKET,
|
||||
matchToken.COMMA,
|
||||
matchToken.COLON,
|
||||
matchToken.OPEN_BRACES,
|
||||
matchToken.OPEN_SQUARE_BRACKET,
|
||||
]);
|
||||
|
||||
export function isTokenCannotBeAtEnd(token: string): boolean {
|
||||
return TOKEN_CANNOT_BE_AT_END_SET.has(token);
|
||||
}
|
||||
|
||||
export const TOKEN_CANNOT_PRECEDE_SUFFIX_TOKEN_SET = new Set<string>([
|
||||
operatorToken.PLUS,
|
||||
operatorToken.MINUS,
|
||||
operatorToken.MULTIPLY,
|
||||
operatorToken.DIVIDED,
|
||||
operatorToken.CONCATENATE,
|
||||
operatorToken.POWER,
|
||||
operatorToken.EQUALS,
|
||||
operatorToken.NOT_EQUAL,
|
||||
operatorToken.GREATER_THAN,
|
||||
operatorToken.GREATER_THAN_OR_EQUAL,
|
||||
operatorToken.LESS_THAN,
|
||||
operatorToken.LESS_THAN_OR_EQUAL,
|
||||
|
||||
matchToken.OPEN_BRACKET,
|
||||
matchToken.COMMA,
|
||||
matchToken.COLON,
|
||||
matchToken.OPEN_BRACES,
|
||||
matchToken.OPEN_SQUARE_BRACKET,
|
||||
|
||||
suffixToken.PERCENTAGE,
|
||||
suffixToken.POUND,
|
||||
]);
|
||||
|
||||
export function isTokenCannotPrecedeSuffixToken(token: string): boolean {
|
||||
return TOKEN_CANNOT_PRECEDE_SUFFIX_TOKEN_SET.has(token);
|
||||
}
|
||||
701
packages/engine-formula/src/basics/math.ts
Normal file
701
packages/engine-formula/src/basics/math.ts
Normal file
@@ -0,0 +1,701 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function calculateFactorial(n: number, step: number = 1): number {
|
||||
let _n = Math.floor(n);
|
||||
|
||||
if (n < 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
let result = 1;
|
||||
|
||||
while (_n > 1 && Number.isFinite(result)) {
|
||||
result *= _n;
|
||||
_n -= step;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function calculateCombin(n: number, k: number): number {
|
||||
const t = Math.min(n - k, k);
|
||||
|
||||
let result = 1;
|
||||
|
||||
for (let i = 1; i <= t; i++) {
|
||||
if (!Number.isFinite(result)) {
|
||||
break;
|
||||
}
|
||||
|
||||
result *= n - i + 1;
|
||||
result /= i;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function calculateGcd(a: number, b: number): number {
|
||||
let _a = Math.floor(a);
|
||||
let _b = Math.floor(b);
|
||||
|
||||
while (_b !== 0) {
|
||||
const t = _b;
|
||||
_b = _a % _b;
|
||||
_a = t;
|
||||
}
|
||||
|
||||
return _a;
|
||||
}
|
||||
|
||||
export function calculateLcm(a: number, b: number): number {
|
||||
const den = calculateGcd(a, b);
|
||||
|
||||
if (den === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.abs(a * b) / den;
|
||||
}
|
||||
|
||||
export function calculateMdeterm(matrix: number[][]): number {
|
||||
const n = matrix.length;
|
||||
|
||||
if (n === 1) {
|
||||
return matrix[0][0];
|
||||
}
|
||||
|
||||
if (n === 2) {
|
||||
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
|
||||
}
|
||||
|
||||
const { rowSwap, smallPivotDetected, luMatrix, permutation } = performLUDecomposition(matrix);
|
||||
|
||||
if (smallPivotDetected) {
|
||||
return 0; // Matrix is irreversible
|
||||
}
|
||||
|
||||
let det = rowSwap ? 1 : -1;
|
||||
|
||||
for (let i = 0; i < permutation.length; i++) {
|
||||
det *= luMatrix[i][i];
|
||||
}
|
||||
|
||||
return det === 0 ? 0 : det; // deal with -0 case
|
||||
}
|
||||
|
||||
export function calculateMinverse(matrix: number[][]): number[][] | null {
|
||||
const det = calculateMdeterm(matrix);
|
||||
|
||||
if (det === 0) {
|
||||
return null; // Matrix is irreversible
|
||||
}
|
||||
|
||||
if (matrix.length === 1) {
|
||||
return [[1 / det]];
|
||||
}
|
||||
|
||||
const adjugate = adjoint(matrix);
|
||||
const inverseMatrix = adjugate.map((row) => row.map((value) => value / det));
|
||||
|
||||
return inverseMatrix;
|
||||
}
|
||||
|
||||
function minor(matrix: number[][], row: number, col: number): number[][] {
|
||||
return matrix
|
||||
.filter((_, r) => r !== row)
|
||||
.map((row) => row.filter((_, c) => c !== col));
|
||||
}
|
||||
|
||||
function adjoint(matrix: number[][]): number[][] {
|
||||
const n = matrix.length;
|
||||
const adj = Array.from({ length: n }, () => new Array(n).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
const sign = ((i + j) % 2 === 0 ? 1 : -1);
|
||||
const res = sign * calculateMdeterm(minor(matrix, i, j));
|
||||
adj[j][i] = (res === 0 ? 0 : res); // deal with -0 case
|
||||
}
|
||||
}
|
||||
|
||||
return adj;
|
||||
}
|
||||
|
||||
export function calculateMmult(matrix1: number[][], matrix2: number[][]): number[][] {
|
||||
return matrix1.map((row) => matrix2[0].map((_, colIndex) =>
|
||||
row.reduce((sum, element, rowIndex) => sum + element * matrix2[rowIndex][colIndex], 0)
|
||||
));
|
||||
}
|
||||
|
||||
export function matrixTranspose(matrix: number[][]): number[][] {
|
||||
return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
|
||||
}
|
||||
|
||||
export function inverseMatrixByLUD(matrix: number[][]) {
|
||||
const { smallPivotDetected, luMatrix, permutation } = performLUDecomposition(matrix);
|
||||
|
||||
if (smallPivotDetected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformMatrix(luMatrix, permutation);
|
||||
}
|
||||
|
||||
function performLUDecomposition(matrix: number[][]) {
|
||||
const decomposedMatrix = matrixTranspose(matrix);
|
||||
const numRows = decomposedMatrix.length;
|
||||
const numCols = decomposedMatrix[0].length;
|
||||
let isRowSwap = true;
|
||||
let smallPivotDetected = false;
|
||||
const luMatrix = createMatrix(numRows, numCols, 0);
|
||||
const permutation = new Array(numCols).fill(0).map((_, index) => index);
|
||||
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
for (let i = 0; i < c; i++) {
|
||||
let value = decomposedMatrix[i][c];
|
||||
|
||||
for (let k = 0; k < i; k++) {
|
||||
value -= luMatrix[i][k] * luMatrix[k][c];
|
||||
}
|
||||
|
||||
luMatrix[i][c] = value;
|
||||
}
|
||||
|
||||
let maxPivot = -Infinity;
|
||||
let pivotRow = c;
|
||||
|
||||
for (let r = c; r < numRows; r++) {
|
||||
let value = decomposedMatrix[r][c];
|
||||
|
||||
for (let k = 0; k < c; k++) {
|
||||
value -= luMatrix[r][k] * luMatrix[k][c];
|
||||
}
|
||||
|
||||
luMatrix[r][c] = value;
|
||||
|
||||
const absValue = Math.abs(value);
|
||||
|
||||
if (absValue > maxPivot) {
|
||||
maxPivot = absValue;
|
||||
pivotRow = r;
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.abs(luMatrix[pivotRow][c]) < 1e-11) {
|
||||
smallPivotDetected = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (pivotRow !== c) {
|
||||
[luMatrix[c], luMatrix[pivotRow]] = [luMatrix[pivotRow], luMatrix[c]];
|
||||
[decomposedMatrix[c], decomposedMatrix[pivotRow]] = [decomposedMatrix[pivotRow], decomposedMatrix[c]];
|
||||
[permutation[c], permutation[pivotRow]] = [permutation[pivotRow], permutation[c]];
|
||||
isRowSwap = !isRowSwap;
|
||||
}
|
||||
|
||||
const pivotElement = luMatrix[c][c];
|
||||
|
||||
for (let r = c + 1; r < numRows; r++) {
|
||||
luMatrix[r][c] /= pivotElement;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rowSwap: isRowSwap,
|
||||
smallPivotDetected,
|
||||
luMatrix,
|
||||
permutation,
|
||||
};
|
||||
}
|
||||
|
||||
function transformMatrix(inputMatrix: number[][], indices: number[]): number[][] {
|
||||
const size = indices.length;
|
||||
const identityMatrix = createMatrix(size, size, 0);
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
identityMatrix[i][i] = 1;
|
||||
}
|
||||
|
||||
const resultMatrix = createMatrix(size, size, 0);
|
||||
|
||||
for (let rowIndex = 0; rowIndex < size; rowIndex++) {
|
||||
const index = indices[rowIndex];
|
||||
for (let colIndex = 0; colIndex < size; colIndex++) {
|
||||
resultMatrix[rowIndex][colIndex] = identityMatrix[index][colIndex];
|
||||
}
|
||||
}
|
||||
|
||||
for (let pivot = 0; pivot < size; pivot++) {
|
||||
const currentRow = resultMatrix[pivot];
|
||||
|
||||
for (let row = pivot + 1; row < size; row++) {
|
||||
const factor = inputMatrix[row][pivot];
|
||||
|
||||
for (let column = 0; column < size; column++) {
|
||||
resultMatrix[row][column] -= currentRow[column] * factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let row = size - 1; row >= 0; row--) {
|
||||
const currentRow = resultMatrix[row];
|
||||
const pivotElement = inputMatrix[row][row];
|
||||
|
||||
for (let column = 0; column < size; column++) {
|
||||
currentRow[column] /= pivotElement;
|
||||
}
|
||||
|
||||
for (let upperRow = 0; upperRow < row; upperRow++) {
|
||||
const upperFactor = inputMatrix[upperRow][row];
|
||||
|
||||
for (let upperCol = 0; upperCol < size; upperCol++) {
|
||||
resultMatrix[upperRow][upperCol] -= currentRow[upperCol] * upperFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultMatrix;
|
||||
}
|
||||
|
||||
function createMatrix(rows: number, cols: number, initialValue: number): number[][] {
|
||||
const matrix: number[][] = [];
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
matrix[r] = [];
|
||||
|
||||
for (let c = 0; c < cols; c++) {
|
||||
matrix[r].push(initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
export function inverseMatrixByUSV(matrix: number[][]): number[][] | null {
|
||||
const matrixUSV = getMatrixUSV(matrix);
|
||||
|
||||
if (!matrixUSV) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { matrixU, matrixS, matrixV } = matrixUSV;
|
||||
|
||||
const matrixUT = matrixTranspose(matrixU);
|
||||
const newMatrix = Array.from({ length: matrixS.length }, () => new Array(matrix[0].length).fill(0));
|
||||
const EPSILON = Math.max(matrix.length, matrix[0].length) * Number.EPSILON * matrixS[0];
|
||||
|
||||
for (let i = 0; i < matrixS.length; i++) {
|
||||
if (Math.abs(matrixS[i]) > EPSILON) {
|
||||
newMatrix[i][i] = 1 / matrixS[i];
|
||||
}
|
||||
}
|
||||
|
||||
return calculateMmult(matrixV, calculateMmult(newMatrix, matrixUT));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
function getMatrixUSV(matrix: number[][]) {
|
||||
const matrixU = matrixTranspose(matrix);
|
||||
const m = matrixU.length;
|
||||
const n = matrixU[0].length;
|
||||
|
||||
if (m < n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matrixF = new Array(n).fill(0);
|
||||
const matrixS = new Array(n).fill(0);
|
||||
const matrixV = Array.from({ length: n }, () => new Array(n).fill(0));
|
||||
|
||||
let EPSILON = Number.EPSILON;
|
||||
let sqrt = 0;
|
||||
let msp = 0;
|
||||
let maxCoeffecient = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
matrixF[i] = sqrt;
|
||||
msp = getMatrixSumProductOfRows(matrixU, i, m, i, i);
|
||||
|
||||
if (msp <= 1e-64 / EPSILON) {
|
||||
sqrt = 0;
|
||||
} else {
|
||||
sqrt = Math.sqrt(msp);
|
||||
|
||||
if (matrixU[i][i] >= 0) {
|
||||
sqrt = -sqrt;
|
||||
}
|
||||
|
||||
const temp = matrixU[i][i] * sqrt - msp;
|
||||
matrixU[i][i] -= sqrt;
|
||||
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
msp = getMatrixSumProductOfRows(matrixU, i, m, i, j);
|
||||
|
||||
for (let k = i; k < m; k++) {
|
||||
matrixU[k][j] += msp / temp * matrixU[k][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matrixS[i] = sqrt;
|
||||
|
||||
msp = getMatrixSumProductOfCols(matrixU, i + 1, n, i, i);
|
||||
|
||||
if (msp <= 1e-64 / EPSILON) {
|
||||
sqrt = 0;
|
||||
} else {
|
||||
sqrt = Math.sqrt(msp);
|
||||
|
||||
if (matrixU[i][i + 1] >= 0) {
|
||||
sqrt = -sqrt;
|
||||
}
|
||||
|
||||
const temp = matrixU[i][i + 1] * sqrt - msp;
|
||||
matrixU[i][i + 1] -= sqrt;
|
||||
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
matrixF[j] = matrixU[i][j] / temp;
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < m; j++) {
|
||||
msp = getMatrixSumProductOfCols(matrixU, i + 1, n, j, i);
|
||||
|
||||
for (let k = i + 1; k < n; k++) {
|
||||
matrixU[j][k] += msp * matrixF[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const coefficient = Math.abs(matrixS[i]) + Math.abs(matrixF[i]);
|
||||
|
||||
if (coefficient > maxCoeffecient) {
|
||||
maxCoeffecient = coefficient;
|
||||
}
|
||||
}
|
||||
|
||||
let o = 0;
|
||||
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
if (sqrt !== 0) {
|
||||
for (let j = o; j < n; j++) {
|
||||
matrixV[j][i] = matrixU[i][j] / (sqrt * matrixU[i][i + 1]);
|
||||
}
|
||||
|
||||
for (let j = o; j < n; j++) {
|
||||
msp = 0;
|
||||
|
||||
for (let k = o; k < n; k++) {
|
||||
msp += matrixU[i][k] * matrixV[k][j];
|
||||
}
|
||||
|
||||
for (let k = o; k < n; k++) {
|
||||
matrixV[k][j] += msp * matrixV[k][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = o; j < n; j++) {
|
||||
matrixV[i][j] = 0;
|
||||
matrixV[j][i] = 0;
|
||||
}
|
||||
|
||||
matrixV[i][i] = 1;
|
||||
sqrt = matrixF[i];
|
||||
o = i;
|
||||
}
|
||||
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
sqrt = matrixS[i];
|
||||
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
matrixU[i][j] = 0;
|
||||
}
|
||||
|
||||
if (sqrt !== 0) {
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
msp = getMatrixSumProductOfRows(matrixU, i + 1, m, i, j);
|
||||
|
||||
for (let k = i; k < m; k++) {
|
||||
matrixU[k][j] += msp / (matrixU[i][i] * sqrt) * matrixU[k][i];
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = i; j < m; j++) {
|
||||
matrixU[j][i] /= sqrt;
|
||||
}
|
||||
} else {
|
||||
for (let j = i; j < m; j++) {
|
||||
matrixU[j][i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
matrixU[i][i] += 1;
|
||||
}
|
||||
|
||||
EPSILON *= maxCoeffecient;
|
||||
|
||||
let a = 0;
|
||||
let b = 0;
|
||||
let ratio = 0;
|
||||
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
for (let S = 0; S < 50; S++) {
|
||||
let isNeedHandle = false;
|
||||
let index = i;
|
||||
|
||||
for (; index >= 0; index--) {
|
||||
if (Math.abs(matrixF[index]) <= EPSILON) {
|
||||
isNeedHandle = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Math.abs(matrixS[index - 1]) <= EPSILON) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNeedHandle) {
|
||||
let temp1 = 0;
|
||||
let temp2 = 1;
|
||||
|
||||
for (let j = index; j < i + 1; j++) {
|
||||
a = temp2 * matrixF[j];
|
||||
b = matrixS[j];
|
||||
matrixF[j] *= temp1;
|
||||
|
||||
if (Math.abs(a) <= EPSILON) {
|
||||
break;
|
||||
}
|
||||
|
||||
ratio = computeHypotenuse(a, b);
|
||||
matrixS[j] = ratio;
|
||||
temp1 = b / ratio;
|
||||
temp2 = -a / ratio;
|
||||
|
||||
for (let k = 0; k < m; k++) {
|
||||
const value1 = matrixU[k][index - 1];
|
||||
const value2 = matrixU[k][j];
|
||||
|
||||
matrixU[k][index - 1] = value1 * temp1 + value2 * temp2;
|
||||
matrixU[k][j] = -value1 * temp2 + value2 * temp1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index === i) {
|
||||
if (matrixS[i] < 0) {
|
||||
matrixS[i] = -matrixS[i];
|
||||
|
||||
for (let j = 0; j < n; j++) {
|
||||
matrixV[j][i] = -matrixV[j][i];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (S >= 49) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let indexValue = matrixS[index];
|
||||
|
||||
a = ((matrixS[i - 1] - matrixS[i]) * (matrixS[i - 1] + matrixS[i]) + (matrixF[i - 1] - matrixF[i]) * (matrixF[i - 1] + matrixF[i])) / (2 * matrixF[i] * matrixS[i - 1]);
|
||||
ratio = computeHypotenuse(a, 1);
|
||||
|
||||
if (a < 0) {
|
||||
a = ((indexValue - matrixS[i]) * (indexValue + matrixS[i]) + matrixF[i] * (matrixS[i - 1] / (a - ratio) - matrixF[i])) / indexValue;
|
||||
} else {
|
||||
a = ((indexValue - matrixS[i]) * (indexValue + matrixS[i]) + matrixF[i] * (matrixS[i - 1] / (a + ratio) - matrixF[i])) / indexValue;
|
||||
}
|
||||
|
||||
let temp1 = 1;
|
||||
let temp2 = 1;
|
||||
|
||||
for (let j = index + 1; j < i + 1; j++) {
|
||||
let matrixFValue = matrixF[j];
|
||||
let matrixSValue = matrixS[j];
|
||||
|
||||
b = temp2 * matrixFValue;
|
||||
matrixFValue *= temp1;
|
||||
ratio = computeHypotenuse(a, b);
|
||||
matrixF[j - 1] = ratio;
|
||||
|
||||
temp1 = a / ratio;
|
||||
temp2 = b / ratio;
|
||||
a = indexValue * temp1 + matrixFValue * temp2;
|
||||
b = matrixSValue * temp2;
|
||||
matrixFValue = -indexValue * temp2 + matrixFValue * temp1;
|
||||
matrixSValue *= temp1;
|
||||
|
||||
for (let k = 0; k < n; k++) {
|
||||
const value1 = matrixV[k][j - 1];
|
||||
const value2 = matrixV[k][j];
|
||||
|
||||
matrixV[k][j - 1] = value1 * temp1 + value2 * temp2;
|
||||
matrixV[k][j] = -value1 * temp2 + value2 * temp1;
|
||||
}
|
||||
|
||||
ratio = computeHypotenuse(a, b);
|
||||
matrixS[j - 1] = ratio;
|
||||
temp1 = a / ratio;
|
||||
temp2 = b / ratio;
|
||||
a = temp1 * matrixFValue + temp2 * matrixSValue;
|
||||
indexValue = -temp2 * matrixFValue + temp1 * matrixSValue;
|
||||
|
||||
for (let k = 0; k < m; k++) {
|
||||
const value1 = matrixU[k][j - 1];
|
||||
const value2 = matrixU[k][j];
|
||||
|
||||
matrixU[k][j - 1] = value1 * temp1 + value2 * temp2;
|
||||
matrixU[k][j] = -value1 * temp2 + value2 * temp1;
|
||||
}
|
||||
}
|
||||
|
||||
matrixF[index] = 0;
|
||||
matrixF[i] = a;
|
||||
matrixS[i] = indexValue;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < matrixS.length; i++) {
|
||||
if (matrixS[i] < EPSILON) {
|
||||
matrixS[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (matrixS[j] < matrixS[i]) {
|
||||
const temp = matrixS[j];
|
||||
matrixS[j] = matrixS[i];
|
||||
matrixS[i] = temp;
|
||||
|
||||
for (let k = 0; k < matrixU.length; k++) {
|
||||
const temp = matrixU[k][i];
|
||||
matrixU[k][i] = matrixU[k][j];
|
||||
matrixU[k][j] = temp;
|
||||
}
|
||||
|
||||
for (let k = 0; k < matrixV.length; k++) {
|
||||
const temp = matrixV[k][i];
|
||||
matrixV[k][i] = matrixV[k][j];
|
||||
matrixV[k][j] = temp;
|
||||
}
|
||||
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
matrixU,
|
||||
matrixS,
|
||||
matrixV,
|
||||
};
|
||||
}
|
||||
|
||||
function computeHypotenuse(a: number, b: number): number {
|
||||
let ratio = 0;
|
||||
|
||||
if (Math.abs(a) > Math.abs(b)) {
|
||||
ratio = b / a;
|
||||
return Math.abs(a) * Math.sqrt(1 + ratio * ratio);
|
||||
}
|
||||
|
||||
if (b !== 0) {
|
||||
ratio = a / b;
|
||||
return Math.abs(b) * Math.sqrt(1 + ratio * ratio);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getMatrixSumProductOfRows(matrix: number[][], startRow: number, endRow: number, col1: number, col2: number): number {
|
||||
let sum = 0;
|
||||
|
||||
for (let i = startRow; i < endRow; i++) {
|
||||
sum += matrix[i][col1] * matrix[i][col2];
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
function getMatrixSumProductOfCols(matrix: number[][], startCol: number, endCol: number, row1: number, row2: number): number {
|
||||
let sum = 0;
|
||||
|
||||
for (let i = startCol; i < endCol; i++) {
|
||||
sum += matrix[row1][i] * matrix[row2][i];
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
export const romanToArabicMap = new Map<string, number>([
|
||||
['I', 1],
|
||||
['V', 5],
|
||||
['X', 10],
|
||||
['L', 50],
|
||||
['C', 100],
|
||||
['D', 500],
|
||||
['M', 1000],
|
||||
]);
|
||||
|
||||
export const arabicToRomanMap = new Map<number, string>([
|
||||
[1, 'I'],
|
||||
[4, 'IV'],
|
||||
[5, 'V'],
|
||||
[9, 'IX'],
|
||||
[10, 'X'],
|
||||
[40, 'XL'],
|
||||
[45, 'VL'],
|
||||
[49, 'IL'],
|
||||
[50, 'L'],
|
||||
[90, 'XC'],
|
||||
[95, 'VC'],
|
||||
[99, 'IC'],
|
||||
[100, 'C'],
|
||||
[400, 'CD'],
|
||||
[450, 'LD'],
|
||||
[490, 'XD'],
|
||||
[495, 'VD'],
|
||||
[499, 'ID'],
|
||||
[500, 'D'],
|
||||
[900, 'CM'],
|
||||
[950, 'LM'],
|
||||
[990, 'XM'],
|
||||
[995, 'VM'],
|
||||
[999, 'IM'],
|
||||
[1000, 'M'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* form: A number specifying the type of roman numeral you want.
|
||||
* The roman numeral style ranges from Classic to Simplified, becoming more concise as the value of form increases
|
||||
* 0 Classic
|
||||
* 1 More concise
|
||||
* 2 More concise
|
||||
* 3 More concise
|
||||
* 4 Simplified
|
||||
*/
|
||||
export const romanFormArray: number[][] = [
|
||||
[1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 4000],
|
||||
[1, 4, 5, 9, 10, 40, 45, 50, 90, 95, 100, 400, 450, 500, 900, 950, 1000, 4000],
|
||||
[1, 4, 5, 9, 10, 40, 45, 49, 50, 90, 95, 99, 100, 400, 450, 490, 500, 900, 950, 990, 1000, 4000],
|
||||
[1, 4, 5, 9, 10, 40, 45, 49, 50, 90, 95, 99, 100, 400, 450, 490, 495, 500, 900, 950, 990, 995, 1000, 4000],
|
||||
[1, 4, 5, 9, 10, 40, 45, 49, 50, 90, 95, 99, 100, 400, 450, 490, 495, 499, 500, 900, 950, 990, 995, 999, 1000, 4000],
|
||||
];
|
||||
63
packages/engine-formula/src/basics/object-class-type.ts
Normal file
63
packages/engine-formula/src/basics/object-class-type.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export class ObjectClassType {
|
||||
pattern: string = '';
|
||||
|
||||
dispose() {
|
||||
|
||||
}
|
||||
|
||||
getPattern() {
|
||||
return this.pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used in NumberValueObject
|
||||
* @param pattern
|
||||
*/
|
||||
setPattern(pattern: string) {
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
isError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isAsyncObject() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isAsyncArrayObject() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isReferenceObject() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isArray() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isValueObject() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isEqualType(object: ObjectClassType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
171
packages/engine-formula/src/basics/regex.ts
Normal file
171
packages/engine-formula/src/basics/regex.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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 { prefixToken, suffixToken } from './token';
|
||||
|
||||
// export const UNIT_NAME_REGEX = '\'?\\[((?![\\/?:"<>|*\\\\]).)*\\]';
|
||||
// export const UNIT_NAME_REGEX = '\'?\\[((?![\\/?:"<>|*\\\\]).)*?\\]';
|
||||
// export const UNIT_NAME_REGEX = '\\[((?![\\/?:"<>|*\\\\]).)*?\\]';
|
||||
|
||||
export const UNIT_NAME_REGEX = '\\[([^\\[\\]\\/?:"<>|*\\\\]+)\\]'; // '[Book-1.xlsx]Sheet1'!$A$4 gets [Book-1.xlsx] as unitId
|
||||
|
||||
export const UNIT_NAME_REGEX_PRECOMPILING = new RegExp(UNIT_NAME_REGEX);
|
||||
|
||||
export const SHEET_NAME_REGEX = '((?![\\[\\]\\/?*\\\\]).)*!';
|
||||
|
||||
export const ABSOLUTE_SYMBOL = '$';
|
||||
|
||||
export const RANGE_SYMBOL = '\\s*?:\\s*?';
|
||||
|
||||
// Column numbers range from A to Z, and row numbers start at 1
|
||||
const COLUMN_REGEX = '[A-Za-z]+';
|
||||
const ROW_REGEX = '[1-9][0-9]*';
|
||||
|
||||
export const UNIT_NAME_SHEET_NAME_REGEX = `'?(${UNIT_NAME_REGEX})?(${SHEET_NAME_REGEX})?'?`;
|
||||
|
||||
export const SIMPLE_SINGLE_RANGE_REGEX = `\\${ABSOLUTE_SYMBOL}?${COLUMN_REGEX}\\${ABSOLUTE_SYMBOL}?${ROW_REGEX}`;
|
||||
|
||||
export const REFERENCE_MULTIPLE_RANGE_REGEX = `^(${prefixToken.AT})?${UNIT_NAME_SHEET_NAME_REGEX}${SIMPLE_SINGLE_RANGE_REGEX}${RANGE_SYMBOL}${SIMPLE_SINGLE_RANGE_REGEX}$`;
|
||||
|
||||
export const REFERENCE_MULTIPLE_RANGE_REGEX_PRECOMPILING = new RegExp(REFERENCE_MULTIPLE_RANGE_REGEX);
|
||||
|
||||
export const REFERENCE_SINGLE_RANGE_REGEX = `^${UNIT_NAME_SHEET_NAME_REGEX}\\s*?${SIMPLE_SINGLE_RANGE_REGEX}(${suffixToken.POUND})?$`;
|
||||
|
||||
export const REFERENCE_SINGLE_RANGE_REGEX_PRECOMPILING = new RegExp(REFERENCE_SINGLE_RANGE_REGEX);
|
||||
|
||||
export const REFERENCE_REGEX_ROW = `^${UNIT_NAME_SHEET_NAME_REGEX}\\${ABSOLUTE_SYMBOL}?${ROW_REGEX}${RANGE_SYMBOL}\\${ABSOLUTE_SYMBOL}?${ROW_REGEX}$`;
|
||||
|
||||
export const REFERENCE_REGEX_ROW_PRECOMPILING = new RegExp(REFERENCE_REGEX_ROW);
|
||||
|
||||
export const REFERENCE_REGEX_COLUMN = `^${UNIT_NAME_SHEET_NAME_REGEX}\\${ABSOLUTE_SYMBOL}?${COLUMN_REGEX}${RANGE_SYMBOL}\\${ABSOLUTE_SYMBOL}?${COLUMN_REGEX}$`;
|
||||
|
||||
export const REFERENCE_REGEX_COLUMN_PRECOMPILING = new RegExp(REFERENCE_REGEX_COLUMN);
|
||||
|
||||
export const REFERENCE_REGEX_SINGLE_ROW = `^${UNIT_NAME_SHEET_NAME_REGEX}\\s*?\\${ABSOLUTE_SYMBOL}?${ROW_REGEX}$`;
|
||||
|
||||
export const REFERENCE_REGEX_SINGLE_ROW_PRECOMPILING = new RegExp(REFERENCE_REGEX_SINGLE_ROW);
|
||||
|
||||
export const REFERENCE_REGEX_SINGLE_COLUMN = `^${UNIT_NAME_SHEET_NAME_REGEX}\\s*?\\${ABSOLUTE_SYMBOL}?${COLUMN_REGEX}$`;
|
||||
|
||||
export const REFERENCE_REGEX_SINGLE_COLUMN_PRECOMPILING = new RegExp(REFERENCE_REGEX_SINGLE_COLUMN);
|
||||
|
||||
const TABLE_NAME_REGEX = '((?![~!@#$%^&*()+<>?:,./;’,。、‘:“《》?~!@#¥%……()【】\\[\\]\\/\\\\]).)+';
|
||||
|
||||
const TABLE_TITLE_REGEX = '\\[#.+\\]\\s*?,\\s*?';
|
||||
|
||||
const TABLE_CONTENT_REGEX = '\\[[^#]*#?\\]';
|
||||
|
||||
const TABLE_MULTIPLE_COLUMN_REGEX = `${TABLE_CONTENT_REGEX}${RANGE_SYMBOL}${TABLE_CONTENT_REGEX}`;
|
||||
|
||||
// Display formulas use Book!Table[Column], OOXML formulas use [n]!Table[Column],
|
||||
// and the legacy runtime-id form [unitId]Table[Column] remains accepted.
|
||||
// Univer display names are not Excel file names: characters such as `|` are valid,
|
||||
// and `]` is escaped as `]]` by the Formula reference builder.
|
||||
const TABLE_BRACKETED_UNIT_QUALIFIER_REGEX = '\\[(?:[^\\]]|\\]\\])+\\]';
|
||||
const TABLE_UNIT_QUALIFIER_REGEX = `(?:(?:${TABLE_BRACKETED_UNIT_QUALIFIER_REGEX}|'(?:[^']|'')+'|[^\\s!\\[\\]]+)!)?(?:${TABLE_BRACKETED_UNIT_QUALIFIER_REGEX})?`;
|
||||
|
||||
export const REFERENCE_TABLE_ALL_COLUMN_REGEX = `^${TABLE_UNIT_QUALIFIER_REGEX}${TABLE_NAME_REGEX}$`;
|
||||
|
||||
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_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);
|
||||
|
||||
export const REFERENCE_TABLE_SINGLE_COLUMN_REGEX_PRECOMPILING = new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX);
|
||||
|
||||
export const REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX_PRECOMPILING = new RegExp(REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX);
|
||||
|
||||
export const REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX_PRECOMPILING = new RegExp(REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX);
|
||||
|
||||
export const SUPER_TABLE_COLUMN_REGEX = '\\[[^\\]]*?]';
|
||||
|
||||
export const SUPER_TABLE_COLUMN_REGEX_PRECOMPILING = new RegExp(SUPER_TABLE_COLUMN_REGEX, 'g');
|
||||
|
||||
export const ARRAY_VALUE_REGEX = '{.*?}';
|
||||
|
||||
export const ARRAY_VALUE_REGEX_PRECOMPILING = new RegExp(ARRAY_VALUE_REGEX, 'g');
|
||||
|
||||
export function regexTestSingeRange(token: string): boolean {
|
||||
REFERENCE_SINGLE_RANGE_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_SINGLE_RANGE_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestMultipleRange(token: string): boolean {
|
||||
REFERENCE_MULTIPLE_RANGE_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_MULTIPLE_RANGE_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestRow(token: string): boolean {
|
||||
REFERENCE_REGEX_ROW_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_REGEX_ROW_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestColumn(token: string): boolean {
|
||||
REFERENCE_REGEX_COLUMN_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_REGEX_COLUMN_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestSingleRow(token: string): boolean {
|
||||
REFERENCE_REGEX_SINGLE_ROW_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_REGEX_SINGLE_ROW_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestSingleColumn(token: string): boolean {
|
||||
REFERENCE_REGEX_SINGLE_COLUMN_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_REGEX_SINGLE_COLUMN_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestSuperTableColumn(token: string): boolean {
|
||||
SUPER_TABLE_COLUMN_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return SUPER_TABLE_COLUMN_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestReferenceTableAllColumn(token: string): boolean {
|
||||
REFERENCE_TABLE_ALL_COLUMN_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_TABLE_ALL_COLUMN_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestReferenceTableSingleColumn(token: string): boolean {
|
||||
REFERENCE_TABLE_SINGLE_COLUMN_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_TABLE_SINGLE_COLUMN_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestReferenceTableMultipleColumn(token: string): boolean {
|
||||
REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestReferenceTableTitleOnlyAnyHash(token: string): boolean {
|
||||
REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function regexTestArrayValue(token: string): boolean {
|
||||
ARRAY_VALUE_REGEX_PRECOMPILING.lastIndex = 0;
|
||||
return ARRAY_VALUE_REGEX_PRECOMPILING.test(token);
|
||||
}
|
||||
|
||||
export function isReferenceString(refString: string) {
|
||||
return (
|
||||
regexTestSingeRange(refString) ||
|
||||
regexTestMultipleRange(refString) ||
|
||||
regexTestRow(refString) ||
|
||||
regexTestColumn(refString)
|
||||
);
|
||||
}
|
||||
74
packages/engine-formula/src/basics/runtime.ts
Normal file
74
packages/engine-formula/src/basics/runtime.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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, IObjectMatrixPrimitiveType, Nullable } from '@univerjs/core';
|
||||
import type { IArrayFormulaUnitCellType, IRuntimeUnitDataPrimitiveType, IRuntimeUnitDataType } from './common';
|
||||
import { ObjectMatrix } from '@univerjs/core';
|
||||
|
||||
export function convertUnitDataToRuntime(unitData: IArrayFormulaUnitCellType) {
|
||||
const arrayFormulaCellData: IRuntimeUnitDataType = {};
|
||||
Object.keys(unitData).forEach((unitId) => {
|
||||
const sheetData = unitData[unitId];
|
||||
|
||||
if (sheetData == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (arrayFormulaCellData[unitId] == null) {
|
||||
arrayFormulaCellData[unitId] = {};
|
||||
}
|
||||
|
||||
Object.keys(sheetData).forEach((sheetId) => {
|
||||
const cellData = sheetData[sheetId];
|
||||
|
||||
arrayFormulaCellData[unitId]![sheetId] = new ObjectMatrix(cellData);
|
||||
});
|
||||
});
|
||||
|
||||
return arrayFormulaCellData;
|
||||
}
|
||||
|
||||
export function convertRuntimeToUnitData(unitData: IRuntimeUnitDataType) {
|
||||
const unitPrimitiveData: IRuntimeUnitDataPrimitiveType = {};
|
||||
|
||||
for (const unitId in unitData) {
|
||||
const sheetData = unitData[unitId];
|
||||
if (sheetData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unitPrimitiveData[unitId] == null) {
|
||||
unitPrimitiveData[unitId] = {};
|
||||
}
|
||||
|
||||
for (const sheetId in sheetData) {
|
||||
const cellData = sheetData[sheetId];
|
||||
const primitiveData: IObjectMatrixPrimitiveType<Nullable<ICellData>> = {};
|
||||
|
||||
cellData.forValue((row, column, value) => {
|
||||
if (primitiveData[row] === undefined) {
|
||||
primitiveData[row] = {};
|
||||
}
|
||||
|
||||
primitiveData[row][column] = value;
|
||||
});
|
||||
|
||||
unitPrimitiveData[unitId][sheetId] = primitiveData;
|
||||
}
|
||||
}
|
||||
|
||||
return unitPrimitiveData;
|
||||
}
|
||||
1642
packages/engine-formula/src/basics/statistical.ts
Normal file
1642
packages/engine-formula/src/basics/statistical.ts
Normal file
File diff suppressed because it is too large
Load Diff
33
packages/engine-formula/src/basics/token-type.ts
Normal file
33
packages/engine-formula/src/basics/token-type.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const DEFAULT_TOKEN_TYPE_PARAMETER = 'P_1';
|
||||
|
||||
export const DEFAULT_TOKEN_TYPE_ROOT = 'R_1';
|
||||
|
||||
export const DEFAULT_TOKEN_TYPE_LAMBDA_PARAMETER = 'L_1';
|
||||
|
||||
export const DEFAULT_TOKEN_TYPE_LAMBDA_RUNTIME_PARAMETER = 'LR_1';
|
||||
|
||||
export const DEFAULT_TOKEN_TYPE_LAMBDA_OMIT_PARAMETER = 'LO_1';
|
||||
|
||||
export const DEFAULT_TOKEN_LET_FUNCTION_NAME = 'LET';
|
||||
|
||||
export const DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME = 'LAMBDA';
|
||||
|
||||
export const DEFAULT_TOKEN_CUBE_FUNCTION_NAME = 'CUBE';
|
||||
|
||||
export const FORCED_RECALCULATION_FUNCTION_NAME = new Set<string>(['RAND', 'RANDBETWEEN', 'NOW', 'TODAY']);
|
||||
93
packages/engine-formula/src/basics/token.ts
Normal file
93
packages/engine-formula/src/basics/token.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum operatorToken {
|
||||
PLUS = '+',
|
||||
MINUS = '-',
|
||||
MULTIPLY = '*',
|
||||
DIVIDED = '/',
|
||||
CONCATENATE = '&',
|
||||
POWER = '^',
|
||||
EQUALS = '=',
|
||||
NOT_EQUAL = '<>',
|
||||
GREATER_THAN = '>',
|
||||
GREATER_THAN_OR_EQUAL = '>=',
|
||||
LESS_THAN = '<',
|
||||
LESS_THAN_OR_EQUAL = '<=',
|
||||
}
|
||||
|
||||
export enum compareToken {
|
||||
EQUALS = '=',
|
||||
NOT_EQUAL = '<>',
|
||||
GREATER_THAN = '>',
|
||||
GREATER_THAN_OR_EQUAL = '>=',
|
||||
LESS_THAN = '<',
|
||||
LESS_THAN_OR_EQUAL = '<=',
|
||||
}
|
||||
|
||||
export const OPERATOR_TOKEN_PRIORITY = new Map([
|
||||
[operatorToken.NOT_EQUAL as string, 4],
|
||||
[operatorToken.LESS_THAN as string, 4],
|
||||
[operatorToken.GREATER_THAN_OR_EQUAL as string, 4],
|
||||
[operatorToken.EQUALS as string, 4],
|
||||
[operatorToken.GREATER_THAN as string, 4],
|
||||
[operatorToken.LESS_THAN_OR_EQUAL as string, 4],
|
||||
[operatorToken.CONCATENATE as string, 3],
|
||||
[operatorToken.PLUS as string, 2],
|
||||
[operatorToken.MINUS as string, 2],
|
||||
[operatorToken.DIVIDED as string, 1],
|
||||
[operatorToken.MULTIPLY as string, 1],
|
||||
[operatorToken.POWER as string, 0], // ^
|
||||
]);
|
||||
|
||||
export const OPERATOR_TOKEN_SET = new Set(OPERATOR_TOKEN_PRIORITY.keys());
|
||||
|
||||
export const OPERATOR_TOKEN_COMPARE_SET = new Set([
|
||||
operatorToken.EQUALS as string,
|
||||
operatorToken.NOT_EQUAL,
|
||||
operatorToken.GREATER_THAN,
|
||||
operatorToken.GREATER_THAN_OR_EQUAL,
|
||||
operatorToken.LESS_THAN,
|
||||
operatorToken.LESS_THAN_OR_EQUAL,
|
||||
]);
|
||||
|
||||
export enum matchToken {
|
||||
OPEN_BRACKET = '(',
|
||||
CLOSE_BRACKET = ')',
|
||||
COMMA = ',',
|
||||
SINGLE_QUOTATION = "'",
|
||||
DOUBLE_QUOTATION = '"',
|
||||
OPEN_BRACES = '{',
|
||||
CLOSE_BRACES = '}',
|
||||
COLON = ':',
|
||||
OPEN_SQUARE_BRACKET = '[',
|
||||
CLOSE_SQUARE_BRACKET = ']',
|
||||
}
|
||||
|
||||
export enum suffixToken {
|
||||
PERCENTAGE = '%',
|
||||
POUND = '#',
|
||||
}
|
||||
|
||||
export const SUFFIX_TOKEN_SET = new Set([suffixToken.PERCENTAGE as string, suffixToken.POUND]);
|
||||
|
||||
export enum prefixToken {
|
||||
AT = '@',
|
||||
MINUS = '-',
|
||||
PLUS = '+',
|
||||
}
|
||||
|
||||
export const SPACE_TOKEN = ' ';
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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, IUniverInstanceService, Univer } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { TableOptionType } from '../../../basics/common';
|
||||
import { DefinedNamesService, IDefinedNamesService } from '../../../services/defined-names.service';
|
||||
import { ISuperTableService, SuperTableService } from '../../../services/super-table.service';
|
||||
import { RegisterFunctionMutation } from '../register-function.mutation';
|
||||
import { SetArrayFormulaDataMutation } from '../set-array-formula-data.mutation';
|
||||
import {
|
||||
RemoveDefinedNameMutation,
|
||||
SetDefinedNameMutation,
|
||||
SetDefinedNameMutationFactory,
|
||||
} from '../set-defined-name.mutation';
|
||||
import { RemoveFeatureCalculationMutation, SetFeatureCalculationMutation } from '../set-feature-calculation.mutation';
|
||||
import {
|
||||
SetCellFormulaDependencyCalculationMutation,
|
||||
SetCellFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetFormulaCalculationStopMutation,
|
||||
SetFormulaDependencyCalculationMutation,
|
||||
SetFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaStringBatchCalculationMutation,
|
||||
SetFormulaStringBatchCalculationResultMutation,
|
||||
SetQueryFormulaDependencyAllMutation,
|
||||
SetQueryFormulaDependencyAllResultMutation,
|
||||
SetQueryFormulaDependencyMutation,
|
||||
SetQueryFormulaDependencyResultMutation,
|
||||
SetTriggerFormulaCalculationStartMutation,
|
||||
} from '../set-formula-calculation.mutation';
|
||||
import { SetFormulaDataMutation } from '../set-formula-data.mutation';
|
||||
import { SetImageFormulaDataMutation } from '../set-image-formula-data.mutation';
|
||||
import {
|
||||
RemoveSuperTableMutation,
|
||||
SetSuperTableMutation,
|
||||
SetSuperTableOptionMutation,
|
||||
} from '../set-super-table.mutation';
|
||||
|
||||
describe('formula reference mutations', () => {
|
||||
let univer: Univer;
|
||||
let commandService: ICommandService;
|
||||
let definedNamesService: IDefinedNamesService;
|
||||
let superTableService: ISuperTableService;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([ISuperTableService, { useClass: SuperTableService }]);
|
||||
|
||||
commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(SetDefinedNameMutation);
|
||||
commandService.registerCommand(RemoveDefinedNameMutation);
|
||||
commandService.registerCommand(SetSuperTableMutation);
|
||||
commandService.registerCommand(RemoveSuperTableMutation);
|
||||
commandService.registerCommand(SetSuperTableOptionMutation);
|
||||
definedNamesService = injector.get(IDefinedNamesService);
|
||||
superTableService = injector.get(ISuperTableService);
|
||||
injector.get(IUniverInstanceService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('sets and removes defined names used by formulas', async () => {
|
||||
const setResult = await commandService.executeCommand(SetDefinedNameMutation.id, {
|
||||
unitId: 'book-1',
|
||||
id: 'name-1',
|
||||
name: ' SalesTotal ',
|
||||
formulaOrRefString: ' Sheet1!A1:A10 ',
|
||||
comment: ' Used by dashboard ',
|
||||
});
|
||||
|
||||
expect(setResult).toBe(true);
|
||||
expect(definedNamesService.getValueById('book-1', 'name-1')).toEqual({
|
||||
id: 'name-1',
|
||||
name: 'SalesTotal',
|
||||
formulaOrRefString: 'Sheet1!A1:A10',
|
||||
comment: 'Used by dashboard',
|
||||
hidden: undefined,
|
||||
localSheetId: undefined,
|
||||
formulaOrRefStringWithPrefix: undefined,
|
||||
});
|
||||
|
||||
const removeResult = await commandService.executeCommand(RemoveDefinedNameMutation.id, {
|
||||
unitId: 'book-1',
|
||||
id: 'name-1',
|
||||
});
|
||||
|
||||
expect(removeResult).toBe(true);
|
||||
expect(definedNamesService.getValueById('book-1', 'name-1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sets and removes structured-reference tables used by formulas', async () => {
|
||||
const tableReference = {
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 10,
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const setResult = await commandService.executeCommand(SetSuperTableMutation.id, {
|
||||
unitId: 'book-1',
|
||||
tableName: 'SalesTable',
|
||||
reference: tableReference,
|
||||
});
|
||||
|
||||
expect(setResult).toBe(true);
|
||||
expect(superTableService.getTable('book-1', 'SalesTable')).toEqual(tableReference);
|
||||
|
||||
const removeResult = await commandService.executeCommand(RemoveSuperTableMutation.id, {
|
||||
unitId: 'book-1',
|
||||
tableName: 'SalesTable',
|
||||
});
|
||||
|
||||
expect(removeResult).toBe(true);
|
||||
expect(superTableService.hasTable('book-1', 'SalesTable')).toBe(false);
|
||||
});
|
||||
|
||||
it('registers localized structured-reference table options', async () => {
|
||||
const result = await commandService.executeCommand(SetSuperTableOptionMutation.id, {
|
||||
tableOption: '数据',
|
||||
tableOptionType: TableOptionType.DATA,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(superTableService.getTableOptionMap().get('数据')).toBe(TableOptionType.DATA);
|
||||
});
|
||||
|
||||
it('returns false for malformed defined-name mutations and restores existing values through undo factory', async () => {
|
||||
await commandService.executeCommand(SetDefinedNameMutation.id, {
|
||||
unitId: 'book-1',
|
||||
id: 'name-1',
|
||||
name: 'Original',
|
||||
formulaOrRefString: 'Sheet1!A1',
|
||||
hidden: true,
|
||||
localSheetId: 'sheet-1',
|
||||
formulaOrRefStringWithPrefix: '=Sheet1!A1',
|
||||
});
|
||||
|
||||
const factoryAccessor = { get: () => definedNamesService };
|
||||
|
||||
expect(SetDefinedNameMutationFactory(factoryAccessor as never, { unitId: 'book-1', id: 'name-1' } as never)).toEqual({
|
||||
id: 'name-1',
|
||||
unitId: 'book-1',
|
||||
name: 'Original',
|
||||
formulaOrRefString: 'Sheet1!A1',
|
||||
comment: undefined,
|
||||
hidden: true,
|
||||
localSheetId: 'sheet-1',
|
||||
formulaOrRefStringWithPrefix: '=Sheet1!A1',
|
||||
});
|
||||
expect(SetDefinedNameMutationFactory(factoryAccessor as never, { unitId: 'book-1', id: 'missing' } as never)).toBeNull();
|
||||
expect(SetDefinedNameMutation.handler(commandService as never, null as never)).toBe(false);
|
||||
expect(RemoveDefinedNameMutation.handler(commandService as never, null as never)).toBe(false);
|
||||
expect(definedNamesService.getValueById('book-1', 'name-1')).toMatchObject({
|
||||
name: 'Original',
|
||||
formulaOrRefString: 'Sheet1!A1',
|
||||
hidden: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps RPC-only formula mutations as successful command messages', () => {
|
||||
const accessor = {} as never;
|
||||
const statelessMutations = [
|
||||
RegisterFunctionMutation,
|
||||
SetArrayFormulaDataMutation,
|
||||
SetFormulaDataMutation,
|
||||
SetImageFormulaDataMutation,
|
||||
SetFeatureCalculationMutation,
|
||||
RemoveFeatureCalculationMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetTriggerFormulaCalculationStartMutation,
|
||||
SetFormulaStringBatchCalculationMutation,
|
||||
SetFormulaStringBatchCalculationResultMutation,
|
||||
SetFormulaCalculationStopMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaDependencyCalculationMutation,
|
||||
SetFormulaDependencyCalculationResultMutation,
|
||||
SetCellFormulaDependencyCalculationMutation,
|
||||
SetCellFormulaDependencyCalculationResultMutation,
|
||||
SetQueryFormulaDependencyMutation,
|
||||
SetQueryFormulaDependencyResultMutation,
|
||||
SetQueryFormulaDependencyAllMutation,
|
||||
SetQueryFormulaDependencyAllResultMutation,
|
||||
];
|
||||
|
||||
expect(statelessMutations.map((mutation) => mutation.handler(accessor, {} as never))).toEqual(
|
||||
statelessMutations.map(() => true)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface IOtherFormulaMarkDirtyParams { [unitId: string]: { [sunUnitId: string]: { [formulaId: string]: boolean } } }
|
||||
|
||||
export const OtherFormulaMarkDirty: IMutation<IOtherFormulaMarkDirtyParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'sheet.mutation.data-validation-formula-mark-dirty',
|
||||
handler() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
/**
|
||||
*
|
||||
* Register function in main thread, then send function to worker thread.
|
||||
*
|
||||
* In the formula engine, the mutation is solely responsible for communication between the worker and the main thread.
|
||||
* It requires setting local to true during execution.
|
||||
*/
|
||||
|
||||
export interface IRegisterFunctionMutationParam {
|
||||
/**
|
||||
* function string, function name
|
||||
*/
|
||||
functions: [[string, string]];
|
||||
}
|
||||
|
||||
export const RegisterFunctionMutation: IMutation<IRegisterFunctionMutationParam> = {
|
||||
id: 'formula.mutation.register-function',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { IAccessor, IMutation } from '@univerjs/core';
|
||||
import type { IArrayFormulaEmbeddedMap, IArrayFormulaRangeType, IArrayFormulaUnitCellType } from '../../basics/common';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface ISetArrayFormulaDataMutationParams {
|
||||
arrayFormulaRange: IArrayFormulaRangeType;
|
||||
arrayFormulaCellData: IArrayFormulaUnitCellType;
|
||||
arrayFormulaEmbedded: IArrayFormulaEmbeddedMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no need to process data here, it is used as the main thread to send data to the worker. The main thread has already updated the data in advance, and there is no need to update it again here.
|
||||
*/
|
||||
export const SetArrayFormulaDataMutation: IMutation<ISetArrayFormulaDataMutationParams> = {
|
||||
id: 'formula.mutation.set-array-formula-data',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor: IAccessor, params: ISetArrayFormulaDataMutationParams) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 { IAccessor, IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IDefinedNamesService } from '../../services/defined-names.service';
|
||||
|
||||
export interface ISetDefinedNameMutationSearchParam {
|
||||
unitId: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ISetDefinedNameMutationParam extends ISetDefinedNameMutationSearchParam {
|
||||
name: string;
|
||||
formulaOrRefString: string;
|
||||
comment?: string;
|
||||
localSheetId?: string;
|
||||
hidden?: boolean;
|
||||
formulaOrRefStringWithPrefix?: string; // for excel
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate undo mutation of a `SetDefinedNameMutation`
|
||||
*/
|
||||
export const SetDefinedNameMutationFactory = (
|
||||
accessor: IAccessor,
|
||||
params: ISetDefinedNameMutationParam
|
||||
): ISetDefinedNameMutationParam | null => {
|
||||
const { unitId, id } = params;
|
||||
const definedNamesService = accessor.get(IDefinedNamesService);
|
||||
|
||||
const definedName = definedNamesService.getValueById(unitId, id);
|
||||
if (!definedName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...definedName,
|
||||
unitId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* In the formula engine, the mutation is solely responsible for communication between the worker and the main thread.
|
||||
* It requires setting local to true during execution.
|
||||
*/
|
||||
export const SetDefinedNameMutation: IMutation<ISetDefinedNameMutationParam> = {
|
||||
id: 'formula.mutation.set-defined-name',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const definedNamesService = accessor.get(IDefinedNamesService);
|
||||
const { id, unitId, name, formulaOrRefString, comment, hidden, localSheetId, formulaOrRefStringWithPrefix } = params as ISetDefinedNameMutationParam;
|
||||
definedNamesService.registerDefinedName(unitId, {
|
||||
id,
|
||||
name: name.trim(),
|
||||
formulaOrRefString: formulaOrRefString.trim(),
|
||||
comment: comment?.trim(),
|
||||
hidden,
|
||||
localSheetId,
|
||||
formulaOrRefStringWithPrefix,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const RemoveDefinedNameMutation: IMutation<ISetDefinedNameMutationParam> = {
|
||||
id: 'formula.mutation.remove-defined-name',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
const definedNamesService = accessor.get(IDefinedNamesService);
|
||||
const { unitId, id } = params as ISetDefinedNameMutationSearchParam;
|
||||
definedNamesService.removeDefinedName(unitId, id);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { IFeatureCalculationManagerParam } from '../../services/feature-calculation-manager.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface ISetFeatureCalculationMutation {
|
||||
featureId: string;
|
||||
calculationParam: IFeatureCalculationManagerParam;
|
||||
}
|
||||
/**
|
||||
* In the formula engine, the mutation is solely responsible for communication between the worker and the main thread.
|
||||
* It requires setting local to true during execution.
|
||||
*/
|
||||
export const SetFeatureCalculationMutation: IMutation<ISetFeatureCalculationMutation> = {
|
||||
id: 'formula.mutation.set-feature-calculation',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export interface IRemoveFeatureCalculationMutationParam {
|
||||
featureIds: string[];
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
|
||||
export const RemoveFeatureCalculationMutation: IMutation<IRemoveFeatureCalculationMutationParam> = {
|
||||
id: 'formula.mutation.remove-feature-calculation',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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 { IExecutionOptions, IMutation, IUnitRange, Nullable } from '@univerjs/core';
|
||||
import type {
|
||||
IFormulaExecuteResultMap,
|
||||
IFormulaStringMap,
|
||||
IRuntimeOtherUnitDataType,
|
||||
IRuntimeUnitDataPrimitiveType,
|
||||
} from '../../basics/common';
|
||||
import type {
|
||||
IFormulaDependencyTreeFullJson,
|
||||
IFormulaDependencyTreeJson,
|
||||
IFormulaDependentsAndInRangeResults,
|
||||
} from '../../engine/dependency/dependency-tree';
|
||||
import type { IFormulaDirtyData } from '../../services/current-data.service';
|
||||
import type { FormulaExecutedStateType, IExecutionInProgressParams } from '../../services/runtime.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface ISetFormulaCalculationStartMutation extends IFormulaDirtyData {
|
||||
options: Nullable<IExecutionOptions>;
|
||||
}
|
||||
|
||||
export interface ISetFormulaStringBatchCalculationMutation {
|
||||
formulas: IFormulaStringMap;
|
||||
}
|
||||
|
||||
export interface ISetFormulaStringBatchCalculationResultMutation {
|
||||
result: IFormulaExecuteResultMap;
|
||||
}
|
||||
|
||||
export interface ISetFormulaStringBatchCalculationResultMutation {
|
||||
result: IFormulaExecuteResultMap;
|
||||
}
|
||||
|
||||
export interface ISetFormulaDependencyCalculationMutation {
|
||||
unitId: string;
|
||||
sheetId: string;
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface ISetFormulaDependencyCalculationResultMutation {
|
||||
result: IFormulaDependencyTreeJson[];
|
||||
}
|
||||
|
||||
export interface ISetCellFormulaDependencyCalculationResultMutation {
|
||||
result: IFormulaDependencyTreeFullJson | undefined;
|
||||
}
|
||||
|
||||
export interface ISetQueryFormulaDependencyMutation {
|
||||
unitRanges: IUnitRange[];
|
||||
isInRange?: boolean;
|
||||
}
|
||||
|
||||
export interface ISetQueryFormulaDependencyResultMutation {
|
||||
result: IFormulaDependencyTreeJson[];
|
||||
}
|
||||
|
||||
export interface ISetQueryFormulaDependencyAllMutation {
|
||||
unitRanges: IUnitRange[];
|
||||
}
|
||||
|
||||
export interface ISetQueryFormulaDependencyAllResultMutation {
|
||||
result: IFormulaDependentsAndInRangeResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: @DR-Univer
|
||||
* Trigger the calculation of the formula and stop the formula
|
||||
*/
|
||||
export const SetFormulaCalculationStartMutation: IMutation<ISetFormulaCalculationStartMutation> = {
|
||||
id: 'formula.mutation.set-formula-calculation-start',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetTriggerFormulaCalculationStartMutation: IMutation<ISetFormulaCalculationStartMutation> = {
|
||||
id: 'formula.mutation.set-trigger-formula-calculation-start',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetFormulaStringBatchCalculationMutation: IMutation<ISetFormulaStringBatchCalculationMutation> = {
|
||||
id: 'formula.mutation.set-formula-string-batch-calculation',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetFormulaStringBatchCalculationResultMutation: IMutation<ISetFormulaStringBatchCalculationResultMutation> = {
|
||||
id: 'formula.mutation.set-formula-string-batch-calculation-result',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export interface ISetFormulaCalculationStopMutation { }
|
||||
|
||||
export const SetFormulaCalculationStopMutation: IMutation<ISetFormulaCalculationStopMutation> = {
|
||||
id: 'formula.mutation.set-formula-calculation-stop',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
// TODO: this name lacks Params
|
||||
export interface ISetFormulaCalculationNotificationMutation {
|
||||
functionsExecutedState?: FormulaExecutedStateType;
|
||||
stageInfo?: IExecutionInProgressParams;
|
||||
}
|
||||
|
||||
export const SetFormulaCalculationNotificationMutation: IMutation<ISetFormulaCalculationNotificationMutation> = {
|
||||
id: 'formula.mutation.set-formula-calculation-notification',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export interface ISetFormulaCalculationResultMutation {
|
||||
unitData: IRuntimeUnitDataPrimitiveType;
|
||||
unitOtherData: IRuntimeOtherUnitDataType;
|
||||
}
|
||||
|
||||
export const SetFormulaCalculationResultMutation: IMutation<ISetFormulaCalculationResultMutation> = {
|
||||
id: 'formula.mutation.set-formula-calculation-result',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetFormulaDependencyCalculationMutation: IMutation<{}> = {
|
||||
id: 'formula.mutation.set-formula-dependency-calculation',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetFormulaDependencyCalculationResultMutation: IMutation<ISetFormulaDependencyCalculationResultMutation> = {
|
||||
id: 'formula.mutation.set-formula-dependency-calculation-result',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetCellFormulaDependencyCalculationMutation: IMutation<ISetFormulaDependencyCalculationMutation> = {
|
||||
id: 'formula.mutation.set-cell-formula-dependency-calculation',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetCellFormulaDependencyCalculationResultMutation: IMutation<ISetCellFormulaDependencyCalculationResultMutation> = {
|
||||
id: 'formula.mutation.set-cell-formula-dependency-calculation-result',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetQueryFormulaDependencyMutation: IMutation<ISetQueryFormulaDependencyMutation> = {
|
||||
id: 'formula.mutation.set-query-formula-dependency',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetQueryFormulaDependencyResultMutation: IMutation<ISetQueryFormulaDependencyResultMutation> = {
|
||||
id: 'formula.mutation.set-query-formula-dependency-result',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetQueryFormulaDependencyAllMutation: IMutation<ISetQueryFormulaDependencyAllMutation> = {
|
||||
id: 'formula.mutation.set-query-formula-dependency-all',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const SetQueryFormulaDependencyAllResultMutation: IMutation<ISetQueryFormulaDependencyAllResultMutation> = {
|
||||
id: 'formula.mutation.set-query-formula-dependency-all-result',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { IAccessor, IMutation } from '@univerjs/core';
|
||||
import type { IFormulaData } from '../../basics/common';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface ISetFormulaDataMutationParams {
|
||||
formulaData: IFormulaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no need to process data here, it is used as the main thread to send data to the worker.
|
||||
* The main thread has already updated the data in advance, and there is no need to update it again here.
|
||||
*/
|
||||
export const SetFormulaDataMutation: IMutation<ISetFormulaDataMutationParams> = {
|
||||
id: 'formula.mutation.set-formula-data',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor: IAccessor, params: ISetFormulaDataMutationParams) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { IAccessor, IMutation } from '@univerjs/core';
|
||||
import type { IRuntimeImageFormulaDataType } from '../../basics/common';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface ISetImageFormulaDataMutationParams {
|
||||
imageFormulaData: IRuntimeImageFormulaDataType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no need to process data here, it is used as the main thread to send data to the worker. The main thread has already updated the data in advance, and there is no need to update it again here.
|
||||
*/
|
||||
export const SetImageFormulaDataMutation: IMutation<ISetImageFormulaDataMutationParams> = {
|
||||
id: 'formula.mutation.set-image-formula-data',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor: IAccessor, params: ISetImageFormulaDataMutationParams) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { IOtherFormulaDataItem } from '../../basics/common';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export interface ISetOtherFormulaMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
formulaMap: Record<string, IOtherFormulaDataItem>;
|
||||
};
|
||||
export interface IRemoveOtherFormulaMutationParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
formulaIdList: string[];
|
||||
};
|
||||
|
||||
// TODO: remove these two mutations to use RPC instead.
|
||||
|
||||
/**
|
||||
* In the formula engine, the mutation is solely responsible for communication between the worker and the main thread.
|
||||
* It requires setting local to true during execution.
|
||||
*/
|
||||
export const SetOtherFormulaMutation: IMutation<ISetOtherFormulaMutationParams> = {
|
||||
id: 'formula.mutation.set-other-formula',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
|
||||
export const RemoveOtherFormulaMutation: IMutation<IRemoveOtherFormulaMutationParams> = {
|
||||
id: 'formula.mutation.remove-other-formula',
|
||||
type: CommandType.MUTATION,
|
||||
handler: () => true,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import type { ISuperTable } from '../../basics/common';
|
||||
import type { ISuperTableOptionParam } from '../../services/super-table.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { ISuperTableService } from '../../services/super-table.service';
|
||||
|
||||
export interface ISetSuperTableMutationSearchParam {
|
||||
unitId: string;
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
export interface IRemoveSuperTableMutationParam extends ISetSuperTableMutationSearchParam {
|
||||
reference?: Pick<ISuperTable, 'sheetId' | 'range'>;
|
||||
}
|
||||
|
||||
export interface ISetSuperTableMutationParam extends ISetSuperTableMutationSearchParam {
|
||||
oldTableName?: string;
|
||||
reference: ISuperTable;
|
||||
}
|
||||
/**
|
||||
* In the formula engine, the mutation is solely responsible for communication between the worker and the main thread.
|
||||
* It requires setting local to true during execution.
|
||||
*/
|
||||
export const SetSuperTableMutation: IMutation<ISetSuperTableMutationParam> = {
|
||||
id: 'formula.mutation.set-super-table',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { unitId, tableName, reference } = params;
|
||||
const superTableService = accessor.get(ISuperTableService);
|
||||
superTableService.registerTable(unitId, tableName, reference);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const RemoveSuperTableMutation: IMutation<IRemoveSuperTableMutationParam> = {
|
||||
id: 'formula.mutation.remove-super-table',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { unitId, tableName } = params;
|
||||
const superTableService = accessor.get(ISuperTableService);
|
||||
superTableService.remove(unitId, tableName);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetSuperTableOptionMutation: IMutation<ISuperTableOptionParam> = {
|
||||
id: 'formula.mutation.set-super-table-option',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const { tableOption, tableOptionType } = params;
|
||||
const superTableService = accessor.get(ISuperTableService);
|
||||
superTableService.registerTableOptionMap(tableOption, tableOptionType);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
42
packages/engine-formula/src/config/config.ts
Normal file
42
packages/engine-formula/src/config/config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { Ctor } from '@univerjs/core';
|
||||
import type { IFunctionNames } from '../basics/function';
|
||||
import type { BaseFunction } from '../functions/base-function';
|
||||
|
||||
export const ENGINE_FORMULA_PLUGIN_CONFIG_KEY = 'engine-formula.config';
|
||||
|
||||
export const DEFAULT_CYCLE_REFERENCE_COUNT = 1;
|
||||
|
||||
export const ENGINE_FORMULA_CYCLE_REFERENCE_COUNT = 'CYCLE_REFERENCE_COUNT';
|
||||
|
||||
export const ENGINE_FORMULA_RETURN_DEPENDENCY_TREE = 'RETURN_DEPENDENCY_TREE';
|
||||
|
||||
export const configSymbol = Symbol(ENGINE_FORMULA_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverEngineFormulaConfig {
|
||||
notExecuteFormula?: boolean;
|
||||
function?: Array<[Ctor<BaseFunction>, IFunctionNames]>;
|
||||
|
||||
/**
|
||||
* The formula calculation quantity interval for waiting for the main thread message in the worker. Each time the formula calculates the `intervalCount` quantity, it will receive a main thread message to support stopping the calculation. Default is 500
|
||||
*/
|
||||
intervalCount?: number;
|
||||
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverEngineFormulaConfig = {};
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import { ObjectMatrix } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SetArrayFormulaDataMutation } from '../../commands/mutations/set-array-formula-data.mutation';
|
||||
import {
|
||||
SetCellFormulaDependencyCalculationMutation,
|
||||
SetCellFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetFormulaCalculationStopMutation,
|
||||
SetFormulaDependencyCalculationMutation,
|
||||
SetFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaStringBatchCalculationMutation,
|
||||
SetFormulaStringBatchCalculationResultMutation,
|
||||
SetQueryFormulaDependencyAllMutation,
|
||||
SetQueryFormulaDependencyAllResultMutation,
|
||||
SetQueryFormulaDependencyMutation,
|
||||
SetQueryFormulaDependencyResultMutation,
|
||||
} from '../../commands/mutations/set-formula-calculation.mutation';
|
||||
import { SetImageFormulaDataMutation } from '../../commands/mutations/set-image-formula-data.mutation';
|
||||
import { FormulaExecutedStateType, FormulaExecuteStageType } from '../../services/runtime.service';
|
||||
import { CalculateController } from '../calculate.controller';
|
||||
|
||||
interface ICommandServiceMock {
|
||||
executeCommand: ReturnType<typeof vi.fn>;
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => { dispose: () => void };
|
||||
emit: (id: string, params?: unknown) => void;
|
||||
}
|
||||
|
||||
function createCommandServiceMock(): ICommandServiceMock {
|
||||
const callbacks = new Set<(commandInfo: ICommandInfo) => void>();
|
||||
return {
|
||||
executeCommand: vi.fn(async () => true),
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => {
|
||||
callbacks.add(callback);
|
||||
return {
|
||||
dispose: () => callbacks.delete(callback),
|
||||
};
|
||||
},
|
||||
emit: (id: string, params?: unknown) => {
|
||||
callbacks.forEach((callback) => callback({ id, params } as ICommandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('CalculateController', () => {
|
||||
it('should dispatch execute and stop events from commands', async () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const executionCompleteListener$ = new Subject<any>();
|
||||
const executionInProgressListener$ = new Subject<any>();
|
||||
const calculateFormulaService = {
|
||||
executionCompleteListener$,
|
||||
executionInProgressListener$,
|
||||
stopFormulaExecution: vi.fn(),
|
||||
execute: vi.fn(),
|
||||
executeFormulas: vi.fn(async () => ({ u: {} })),
|
||||
getAllDependencyJson: vi.fn(async () => [{ treeId: 1 }]),
|
||||
getCellDependencyJson: vi.fn(async () => ({ treeId: 2 })),
|
||||
getInRangeFormulas: vi.fn(async () => [{ treeId: 3 }]),
|
||||
getRangeDependents: vi.fn(async () => [{ treeId: 4 }]),
|
||||
getDependentsAndInRangeFormulas: vi.fn(async () => ({ dependents: [], inRanges: [] })),
|
||||
};
|
||||
const formulaDataModel = {
|
||||
getFormulaData: vi.fn(() => ({ unit: {} })),
|
||||
getArrayFormulaCellData: vi.fn(() => ({ unit: {} })),
|
||||
getArrayFormulaRange: vi.fn(() => ({ unit: {} })),
|
||||
setArrayFormulaRange: vi.fn(),
|
||||
setArrayFormulaCellData: vi.fn(),
|
||||
clearPreviousArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaRange: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new CalculateController(
|
||||
commandService as never,
|
||||
calculateFormulaService as never,
|
||||
formulaDataModel as never
|
||||
);
|
||||
|
||||
commandService.emit(SetFormulaCalculationStopMutation.id, {});
|
||||
expect(calculateFormulaService.stopFormulaExecution).toHaveBeenCalledTimes(1);
|
||||
|
||||
commandService.emit(SetFormulaCalculationStartMutation.id, {
|
||||
forceCalculation: true,
|
||||
dirtyRanges: [],
|
||||
});
|
||||
expect(calculateFormulaService.execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formulaData: { unit: {} },
|
||||
arrayFormulaCellData: { unit: {} },
|
||||
arrayFormulaRange: { unit: {} },
|
||||
forceCalculate: true,
|
||||
})
|
||||
);
|
||||
|
||||
commandService.emit(SetArrayFormulaDataMutation.id, {
|
||||
arrayFormulaRange: { u: { s: {} } },
|
||||
arrayFormulaCellData: { u: { s: {} } },
|
||||
});
|
||||
expect(formulaDataModel.setArrayFormulaRange).toHaveBeenCalled();
|
||||
expect(formulaDataModel.setArrayFormulaCellData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle formula query commands and emit local result mutations', async () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const executionCompleteListener$ = new Subject<any>();
|
||||
const executionInProgressListener$ = new Subject<any>();
|
||||
const calculateFormulaService = {
|
||||
executionCompleteListener$,
|
||||
executionInProgressListener$,
|
||||
stopFormulaExecution: vi.fn(),
|
||||
execute: vi.fn(),
|
||||
executeFormulas: vi.fn(async () => ({ unit: { sheet: {} } })),
|
||||
getAllDependencyJson: vi.fn(async () => [{ treeId: 11 }]),
|
||||
getCellDependencyJson: vi.fn(async () => ({ treeId: 12 })),
|
||||
getInRangeFormulas: vi.fn(async () => [{ treeId: 13 }]),
|
||||
getRangeDependents: vi.fn(async () => [{ treeId: 14 }]),
|
||||
getDependentsAndInRangeFormulas: vi.fn(async () => ({ dependents: [{ treeId: 15 }], inRanges: [{ treeId: 16 }] })),
|
||||
};
|
||||
const formulaDataModel = {
|
||||
getFormulaData: vi.fn(() => ({})),
|
||||
getArrayFormulaCellData: vi.fn(() => ({})),
|
||||
getArrayFormulaRange: vi.fn(() => ({})),
|
||||
setArrayFormulaRange: vi.fn(),
|
||||
setArrayFormulaCellData: vi.fn(),
|
||||
clearPreviousArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaRange: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new CalculateController(
|
||||
commandService as never,
|
||||
calculateFormulaService as never,
|
||||
formulaDataModel as never
|
||||
);
|
||||
|
||||
commandService.emit(SetFormulaStringBatchCalculationMutation.id, {
|
||||
formulas: {
|
||||
unit: {
|
||||
sheet: {
|
||||
1: {
|
||||
1: ['=A1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(calculateFormulaService.executeFormulas).toHaveBeenCalled();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaStringBatchCalculationResultMutation.id,
|
||||
{ result: { unit: { sheet: {} } } },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
|
||||
commandService.emit(SetFormulaDependencyCalculationMutation.id, {});
|
||||
await Promise.resolve();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaDependencyCalculationResultMutation.id,
|
||||
{ result: [{ treeId: 11 }] },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
|
||||
commandService.emit(SetCellFormulaDependencyCalculationMutation.id, {
|
||||
unitId: 'u',
|
||||
sheetId: 's',
|
||||
row: 1,
|
||||
column: 2,
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetCellFormulaDependencyCalculationResultMutation.id,
|
||||
{ result: { treeId: 12 } },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
|
||||
commandService.emit(SetQueryFormulaDependencyMutation.id, {
|
||||
unitRanges: [],
|
||||
isInRange: false,
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetQueryFormulaDependencyResultMutation.id,
|
||||
{ result: [{ treeId: 14 }] },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
|
||||
commandService.emit(SetQueryFormulaDependencyMutation.id, {
|
||||
unitRanges: [],
|
||||
isInRange: true,
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetQueryFormulaDependencyResultMutation.id,
|
||||
{ result: [{ treeId: 13 }] },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
|
||||
commandService.emit(SetQueryFormulaDependencyAllMutation.id, {
|
||||
unitRanges: [],
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetQueryFormulaDependencyAllResultMutation.id,
|
||||
{ result: { dependents: [{ treeId: 15 }], inRanges: [{ treeId: 16 }] } },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit calculation progress and apply successful runtime result', async () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const executionCompleteListener$ = new Subject<any>();
|
||||
const executionInProgressListener$ = new Subject<any>();
|
||||
const calculateFormulaService = {
|
||||
executionCompleteListener$,
|
||||
executionInProgressListener$,
|
||||
stopFormulaExecution: vi.fn(),
|
||||
execute: vi.fn(),
|
||||
executeFormulas: vi.fn(async () => ({})),
|
||||
getAllDependencyJson: vi.fn(async () => []),
|
||||
getCellDependencyJson: vi.fn(async () => undefined),
|
||||
getInRangeFormulas: vi.fn(async () => []),
|
||||
getRangeDependents: vi.fn(async () => []),
|
||||
getDependentsAndInRangeFormulas: vi.fn(async () => ({ dependents: [], inRanges: [] })),
|
||||
};
|
||||
const formulaDataModel = {
|
||||
getFormulaData: vi.fn(() => ({})),
|
||||
getArrayFormulaCellData: vi.fn(() => ({})),
|
||||
getArrayFormulaRange: vi.fn(() => ({})),
|
||||
setArrayFormulaRange: vi.fn(),
|
||||
setArrayFormulaCellData: vi.fn(),
|
||||
clearPreviousArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaRange: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new CalculateController(
|
||||
commandService as never,
|
||||
calculateFormulaService as never,
|
||||
formulaDataModel as never
|
||||
);
|
||||
|
||||
executionInProgressListener$.next({
|
||||
totalFormulasToCalculate: 3,
|
||||
completedFormulasCount: 1,
|
||||
totalArrayFormulasToCalculate: 0,
|
||||
completedArrayFormulasCount: 0,
|
||||
formulaCycleIndex: 0,
|
||||
stage: FormulaExecuteStageType.CURRENTLY_CALCULATING,
|
||||
});
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaCalculationNotificationMutation.id,
|
||||
expect.objectContaining({
|
||||
stageInfo: expect.objectContaining({
|
||||
stage: FormulaExecuteStageType.CURRENTLY_CALCULATING,
|
||||
}),
|
||||
}),
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
|
||||
executionCompleteListener$.next({
|
||||
functionsExecutedState: FormulaExecutedStateType.SUCCESS,
|
||||
unitData: {
|
||||
unit: {
|
||||
sheet: new ObjectMatrix({
|
||||
0: {
|
||||
0: { v: 123 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
unitOtherData: {},
|
||||
arrayFormulaRange: { unit: { sheet: {} } },
|
||||
arrayFormulaCellData: { unit: { sheet: {} } },
|
||||
clearArrayFormulaCellData: {},
|
||||
arrayFormulaEmbedded: { unit: { sheet: {} } },
|
||||
imageFormulaData: [
|
||||
{ unitId: 'unit', sheetId: 'sheet', row: 1, column: 1, imageId: 'img-1' },
|
||||
],
|
||||
runtimeFeatureRange: {},
|
||||
runtimeFeatureCellData: {},
|
||||
dependencyTreeModelData: [{ treeId: 100 }],
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(formulaDataModel.clearPreviousArrayFormulaCellData).toHaveBeenCalled();
|
||||
expect(formulaDataModel.mergeArrayFormulaCellData).toHaveBeenCalled();
|
||||
expect(formulaDataModel.mergeArrayFormulaRange).toHaveBeenCalled();
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetArrayFormulaDataMutation.id,
|
||||
expect.anything(),
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetImageFormulaDataMutation.id,
|
||||
expect.objectContaining({
|
||||
imageFormulaData: expect.any(Array),
|
||||
}),
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaDependencyCalculationResultMutation.id,
|
||||
{ result: [{ treeId: 100 }] },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaCalculationResultMutation.id,
|
||||
expect.anything(),
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should only apply tree result when no formula executed', async () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const executionCompleteListener$ = new Subject<any>();
|
||||
const executionInProgressListener$ = new Subject<any>();
|
||||
const calculateFormulaService = {
|
||||
executionCompleteListener$,
|
||||
executionInProgressListener$,
|
||||
stopFormulaExecution: vi.fn(),
|
||||
execute: vi.fn(),
|
||||
executeFormulas: vi.fn(async () => ({})),
|
||||
getAllDependencyJson: vi.fn(async () => []),
|
||||
getCellDependencyJson: vi.fn(async () => undefined),
|
||||
getInRangeFormulas: vi.fn(async () => []),
|
||||
getRangeDependents: vi.fn(async () => []),
|
||||
getDependentsAndInRangeFormulas: vi.fn(async () => ({ dependents: [], inRanges: [] })),
|
||||
};
|
||||
const formulaDataModel = {
|
||||
getFormulaData: vi.fn(() => ({})),
|
||||
getArrayFormulaCellData: vi.fn(() => ({})),
|
||||
getArrayFormulaRange: vi.fn(() => ({})),
|
||||
setArrayFormulaRange: vi.fn(),
|
||||
setArrayFormulaCellData: vi.fn(),
|
||||
clearPreviousArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaCellData: vi.fn(),
|
||||
mergeArrayFormulaRange: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new CalculateController(
|
||||
commandService as never,
|
||||
calculateFormulaService as never,
|
||||
formulaDataModel as never
|
||||
);
|
||||
|
||||
executionCompleteListener$.next({
|
||||
functionsExecutedState: FormulaExecutedStateType.NOT_EXECUTED,
|
||||
dependencyTreeModelData: [{ treeId: 201 }],
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaDependencyCalculationResultMutation.id,
|
||||
{ result: [{ treeId: 201 }] },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(
|
||||
SetFormulaCalculationNotificationMutation.id,
|
||||
{ functionsExecutedState: FormulaExecutedStateType.NOT_EXECUTED },
|
||||
{ onlyLocal: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SetFormulaCalculationNotificationMutation } from '../../commands/mutations/set-formula-calculation.mutation';
|
||||
import { FormulaExecuteStageType } from '../../services/runtime.service';
|
||||
import { ComputingStatusReporterController } from '../computing-status.controller';
|
||||
|
||||
interface ICommandServiceMock {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => { dispose: () => void };
|
||||
emit: (id: string, params?: unknown) => void;
|
||||
}
|
||||
|
||||
function createCommandServiceMock(): ICommandServiceMock {
|
||||
const callbacks = new Set<(commandInfo: ICommandInfo) => void>();
|
||||
return {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => {
|
||||
callbacks.add(callback);
|
||||
return {
|
||||
dispose: () => callbacks.delete(callback),
|
||||
};
|
||||
},
|
||||
emit: (id: string, params?: unknown) => {
|
||||
callbacks.forEach((callback) => callback({ id, params } as ICommandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ComputingStatusReporterController', () => {
|
||||
it('reports formula calculation progress and completion to the global computing status service', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const registeredSubjects: Array<{ getValue: () => boolean; next: (value: boolean) => void; isStopped: boolean }> = [];
|
||||
const globalComputingStatusService = {
|
||||
pushComputingStatusSubject: (subject: { getValue: () => boolean; next: (value: boolean) => void; isStopped: boolean }) => {
|
||||
registeredSubjects.push(subject);
|
||||
return {
|
||||
dispose: () => {
|
||||
const index = registeredSubjects.indexOf(subject);
|
||||
if (index !== -1) {
|
||||
registeredSubjects.splice(index, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const controller = new ComputingStatusReporterController(commandService as never, globalComputingStatusService as never);
|
||||
|
||||
expect(registeredSubjects).toHaveLength(1);
|
||||
expect(registeredSubjects[0].getValue()).toBe(true);
|
||||
|
||||
commandService.emit(SetFormulaCalculationNotificationMutation.id, {
|
||||
stageInfo: {
|
||||
stage: FormulaExecuteStageType.CURRENTLY_CALCULATING,
|
||||
},
|
||||
});
|
||||
expect(registeredSubjects[0].getValue()).toBe(false);
|
||||
|
||||
commandService.emit(SetFormulaCalculationNotificationMutation.id, {
|
||||
stageInfo: {
|
||||
stage: FormulaExecuteStageType.CALCULATION_COMPLETED,
|
||||
},
|
||||
});
|
||||
expect(registeredSubjects[0].getValue()).toBe(true);
|
||||
|
||||
controller.dispose();
|
||||
expect(registeredSubjects).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ignores unrelated commands and notifications without stage info', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const registeredSubjects: Array<{ getValue: () => boolean; next: (value: boolean) => void; isStopped: boolean }> = [];
|
||||
const globalComputingStatusService = {
|
||||
pushComputingStatusSubject: (subject: { getValue: () => boolean; next: (value: boolean) => void; isStopped: boolean }) => {
|
||||
registeredSubjects.push(subject);
|
||||
return { dispose: () => undefined };
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new ComputingStatusReporterController(commandService as never, globalComputingStatusService as never);
|
||||
|
||||
commandService.emit('unrelated.command', {});
|
||||
commandService.emit(SetFormulaCalculationNotificationMutation.id, {});
|
||||
|
||||
expect(registeredSubjects[0].getValue()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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 * as core from '@univerjs/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaCalculationTriggerService } from '../../services/formula-calculation-trigger.service';
|
||||
import { FormulaCalculationTriggerController } from '../formula-calculation-trigger.controller';
|
||||
|
||||
describe('FormulaCalculationTriggerController', () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('starts immediately in Node.js', () => {
|
||||
vi.spyOn(core, 'isNodeEnv').mockReturnValue(true);
|
||||
const start = vi.fn();
|
||||
const lifecycle$ = new BehaviorSubject(core.LifecycleStages.Ready);
|
||||
|
||||
const injector = new core.Injector([
|
||||
[FormulaCalculationTriggerService, { useValue: { start } }],
|
||||
[core.LifecycleService, { useValue: { lifecycle$ } }],
|
||||
]);
|
||||
const controller = injector.createInstance(FormulaCalculationTriggerController);
|
||||
|
||||
expect(start).toHaveBeenCalledOnce();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('starts after rendering in the browser', () => {
|
||||
vi.spyOn(core, 'isNodeEnv').mockReturnValue(false);
|
||||
const start = vi.fn();
|
||||
const lifecycle$ = new BehaviorSubject(core.LifecycleStages.Ready);
|
||||
|
||||
const injector = new core.Injector([
|
||||
[FormulaCalculationTriggerService, { useValue: { start } }],
|
||||
[core.LifecycleService, { useValue: { lifecycle$ } }],
|
||||
]);
|
||||
const controller = injector.createInstance(FormulaCalculationTriggerController);
|
||||
|
||||
expect(start).not.toHaveBeenCalled();
|
||||
lifecycle$.next(core.LifecycleStages.Rendered);
|
||||
expect(start).toHaveBeenCalledOnce();
|
||||
|
||||
lifecycle$.next(core.LifecycleStages.Steady);
|
||||
expect(start).toHaveBeenCalledOnce();
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 { DataSyncPrimaryController } from '@univerjs/rpc';
|
||||
import {
|
||||
CommandService,
|
||||
ConfigService,
|
||||
DesktopLogService,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
ILogService,
|
||||
Injector,
|
||||
LifecycleService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { OtherFormulaMarkDirty } from '../../commands/mutations/formula.mutation';
|
||||
import { ActiveDirtyManagerService, IActiveDirtyManagerService } from '../../services/active-dirty-manager.service';
|
||||
import { FunctionService, IFunctionService } from '../../services/function.service';
|
||||
import { RegisterOtherFormulaService } from '../../services/register-other-formula.service';
|
||||
import { FormulaController } from '../formula.controller';
|
||||
|
||||
type FormulaDataSyncController = Pick<
|
||||
DataSyncPrimaryController,
|
||||
'registerSyncingMutations' | 'syncUnitMutations' | 'waitForPendingMutations'
|
||||
>;
|
||||
|
||||
function createController(dataSyncPrimaryController?: FormulaDataSyncController): {
|
||||
controller: FormulaController;
|
||||
commandService: ICommandService;
|
||||
registerOtherFormulaService: RegisterOtherFormulaService;
|
||||
} {
|
||||
const injector = new Injector();
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([ICommandService, { useClass: CommandService }]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([IActiveDirtyManagerService, { useClass: ActiveDirtyManagerService }]);
|
||||
injector.add([LifecycleService]);
|
||||
injector.add([RegisterOtherFormulaService]);
|
||||
const registerOtherFormulaService = injector.get(RegisterOtherFormulaService);
|
||||
const commandService = injector.get(ICommandService);
|
||||
return {
|
||||
controller: new FormulaController(
|
||||
commandService,
|
||||
injector.get(IFunctionService),
|
||||
injector.get(IConfigService),
|
||||
dataSyncPrimaryController,
|
||||
registerOtherFormulaService
|
||||
),
|
||||
commandService,
|
||||
registerOtherFormulaService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('FormulaController', () => {
|
||||
it('registers the generic other-formula dirty mutation', () => {
|
||||
const { commandService, controller } = createController();
|
||||
|
||||
expect(commandService.hasCommand(OtherFormulaMarkDirty.id)).toBe(true);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('registers mutation-only synchronization once per other-formula host', () => {
|
||||
const dispose = vi.fn();
|
||||
const dataSyncPrimaryController = {
|
||||
registerSyncingMutations: vi.fn<DataSyncPrimaryController['registerSyncingMutations']>(),
|
||||
syncUnitMutations: vi.fn<DataSyncPrimaryController['syncUnitMutations']>(() => toDisposable(dispose)),
|
||||
waitForPendingMutations: vi.fn<DataSyncPrimaryController['waitForPendingMutations']>(() => Promise.resolve()),
|
||||
};
|
||||
const { controller, registerOtherFormulaService } = createController(dataSyncPrimaryController);
|
||||
|
||||
registerOtherFormulaService.registerFormulaWithRange('doc-1', 'body-1', '=1');
|
||||
registerOtherFormulaService.registerFormulaWithRange('doc-1', 'body-1', '=2');
|
||||
registerOtherFormulaService.registerFormulaWithRange('slide-1', 'page-1', '=3');
|
||||
|
||||
expect(dataSyncPrimaryController.syncUnitMutations).toHaveBeenCalledTimes(2);
|
||||
expect(dataSyncPrimaryController.syncUnitMutations).toHaveBeenNthCalledWith(1, 'doc-1');
|
||||
expect(dataSyncPrimaryController.syncUnitMutations).toHaveBeenNthCalledWith(2, 'slide-1');
|
||||
|
||||
controller.dispose();
|
||||
expect(dispose).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('supports formula registration when RPC data sync is unavailable', () => {
|
||||
const { controller, registerOtherFormulaService } = createController();
|
||||
|
||||
expect(() => {
|
||||
registerOtherFormulaService.registerFormulaWithRange('doc-1', 'body-1', '=1');
|
||||
}).not.toThrow();
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SetDefinedNameMutation } from '../../commands/mutations/set-defined-name.mutation';
|
||||
import {
|
||||
RemoveFeatureCalculationMutation,
|
||||
SetFeatureCalculationMutation,
|
||||
} from '../../commands/mutations/set-feature-calculation.mutation';
|
||||
import { SetFormulaDataMutation } from '../../commands/mutations/set-formula-data.mutation';
|
||||
import {
|
||||
RemoveOtherFormulaMutation,
|
||||
SetOtherFormulaMutation,
|
||||
} from '../../commands/mutations/set-other-formula.mutation';
|
||||
import { SetDependencyController } from '../set-dependency.controller';
|
||||
|
||||
interface ICommandServiceMock {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => { dispose: () => void };
|
||||
emit: (id: string, params?: unknown) => void;
|
||||
}
|
||||
|
||||
function createCommandServiceMock(): ICommandServiceMock {
|
||||
const callbacks = new Set<(commandInfo: ICommandInfo) => void>();
|
||||
return {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => {
|
||||
callbacks.add(callback);
|
||||
return {
|
||||
dispose: () => callbacks.delete(callback),
|
||||
};
|
||||
},
|
||||
emit: (id: string, params?: unknown) => {
|
||||
callbacks.forEach((callback) => callback({ id, params } as ICommandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('SetDependencyController', () => {
|
||||
it('should react to feature manager changes and command mutations', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const onChanged$ = new Subject<{ unitId: string; subUnitId: string; featureIds: string[] }>();
|
||||
|
||||
const dependencyManagerService = {
|
||||
removeFeatureFormulaDependency: vi.fn(),
|
||||
removeOtherFormulaDependency: vi.fn(),
|
||||
clearFormulaDependency: vi.fn(),
|
||||
removeFormulaDependency: vi.fn(),
|
||||
removeFormulaDependencyByDefinedName: vi.fn(),
|
||||
};
|
||||
|
||||
const featureCalculationManagerService = {
|
||||
onChanged$,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetDependencyController(
|
||||
commandService as never,
|
||||
dependencyManagerService as never,
|
||||
featureCalculationManagerService as never
|
||||
);
|
||||
|
||||
onChanged$.next({ unitId: 'u', subUnitId: 's', featureIds: ['f1', 'f2'] });
|
||||
expect(dependencyManagerService.removeFeatureFormulaDependency).toHaveBeenCalledWith('u', 's', ['f1', 'f2']);
|
||||
|
||||
commandService.emit(RemoveFeatureCalculationMutation.id, { unitId: 'u', subUnitId: 's', featureIds: ['f3'] });
|
||||
expect(dependencyManagerService.removeFeatureFormulaDependency).toHaveBeenCalledWith('u', 's', ['f3']);
|
||||
|
||||
commandService.emit(SetFeatureCalculationMutation.id, {
|
||||
featureId: 'f4',
|
||||
calculationParam: {
|
||||
unitId: 'u',
|
||||
subUnitId: 's',
|
||||
},
|
||||
});
|
||||
expect(dependencyManagerService.removeFeatureFormulaDependency).toHaveBeenCalledWith('u', 's', ['f4']);
|
||||
|
||||
commandService.emit(RemoveOtherFormulaMutation.id, {
|
||||
unitId: 'u',
|
||||
subUnitId: 's',
|
||||
formulaIdList: ['fo1'],
|
||||
});
|
||||
expect(dependencyManagerService.removeOtherFormulaDependency).toHaveBeenCalledWith('u', 's', ['fo1']);
|
||||
|
||||
commandService.emit(SetOtherFormulaMutation.id, {
|
||||
unitId: 'u',
|
||||
subUnitId: 's',
|
||||
formulaMap: {
|
||||
a: { f: '=A1', ranges: [] },
|
||||
b: { f: '=B1', ranges: [] },
|
||||
},
|
||||
});
|
||||
expect(dependencyManagerService.removeOtherFormulaDependency).toHaveBeenCalledWith('u', 's', ['a', 'b']);
|
||||
|
||||
commandService.emit(SetDefinedNameMutation.id, {
|
||||
unitId: 'u',
|
||||
name: 'MY_DEFINED_NAME',
|
||||
});
|
||||
expect(dependencyManagerService.removeFormulaDependencyByDefinedName).toHaveBeenCalledWith('u', 'MY_DEFINED_NAME');
|
||||
});
|
||||
|
||||
it('should clear or remove formula dependencies according to formulaData payload', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const dependencyManagerService = {
|
||||
removeFeatureFormulaDependency: vi.fn(),
|
||||
removeOtherFormulaDependency: vi.fn(),
|
||||
clearFormulaDependency: vi.fn(),
|
||||
removeFormulaDependency: vi.fn(),
|
||||
removeFormulaDependencyByDefinedName: vi.fn(),
|
||||
};
|
||||
const featureCalculationManagerService = {
|
||||
onChanged$: new Subject<{ unitId: string; subUnitId: string; featureIds: string[] }>(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetDependencyController(
|
||||
commandService as never,
|
||||
dependencyManagerService as never,
|
||||
featureCalculationManagerService as never
|
||||
);
|
||||
|
||||
commandService.emit(SetFormulaDataMutation.id, {
|
||||
formulaData: {
|
||||
u1: null,
|
||||
u2: {
|
||||
s1: null,
|
||||
s2: {
|
||||
1: {
|
||||
2: { f: '=A1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dependencyManagerService.clearFormulaDependency).toHaveBeenCalledWith('u1');
|
||||
expect(dependencyManagerService.clearFormulaDependency).toHaveBeenCalledWith('u2', 's1');
|
||||
expect(dependencyManagerService.removeFormulaDependency).toHaveBeenCalledWith('u2', 's2', 1, 2);
|
||||
});
|
||||
|
||||
it('should ignore null mutation params', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const dependencyManagerService = {
|
||||
removeFeatureFormulaDependency: vi.fn(),
|
||||
removeOtherFormulaDependency: vi.fn(),
|
||||
clearFormulaDependency: vi.fn(),
|
||||
removeFormulaDependency: vi.fn(),
|
||||
removeFormulaDependencyByDefinedName: vi.fn(),
|
||||
};
|
||||
const featureCalculationManagerService = {
|
||||
onChanged$: new Subject<{ unitId: string; subUnitId: string; featureIds: string[] }>(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetDependencyController(
|
||||
commandService as never,
|
||||
dependencyManagerService as never,
|
||||
featureCalculationManagerService as never
|
||||
);
|
||||
|
||||
commandService.emit(RemoveFeatureCalculationMutation.id, null);
|
||||
commandService.emit(SetFeatureCalculationMutation.id, null);
|
||||
commandService.emit(RemoveOtherFormulaMutation.id, null);
|
||||
commandService.emit(SetOtherFormulaMutation.id, null);
|
||||
commandService.emit(SetDefinedNameMutation.id, null);
|
||||
|
||||
expect(dependencyManagerService.removeFeatureFormulaDependency).not.toHaveBeenCalled();
|
||||
expect(dependencyManagerService.removeOtherFormulaDependency).not.toHaveBeenCalled();
|
||||
expect(dependencyManagerService.removeFormulaDependencyByDefinedName).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { RemoveFeatureCalculationMutation, SetFeatureCalculationMutation } from '../../commands/mutations/set-feature-calculation.mutation';
|
||||
import { SetFeatureCalculationController } from '../set-feature-calculation.controller';
|
||||
|
||||
interface ICommandServiceMock {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => { dispose: () => void };
|
||||
emit: (id: string, params?: unknown) => void;
|
||||
}
|
||||
|
||||
function createCommandServiceMock(): ICommandServiceMock {
|
||||
const callbacks = new Set<(commandInfo: ICommandInfo) => void>();
|
||||
return {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => {
|
||||
callbacks.add(callback);
|
||||
return {
|
||||
dispose: () => callbacks.delete(callback),
|
||||
};
|
||||
},
|
||||
emit: (id: string, params?: unknown) => {
|
||||
callbacks.forEach((callback) => callback({ id, params } as ICommandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('SetFeatureCalculationController', () => {
|
||||
it('registers and removes feature calculation executors from command payloads', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const featureCalculationManagerService = {
|
||||
register: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetFeatureCalculationController(commandService as never, featureCalculationManagerService as never);
|
||||
|
||||
const calculationParam = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
dependencyRanges: [
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
range: {
|
||||
startRow: 1,
|
||||
endRow: 3,
|
||||
startColumn: 2,
|
||||
endColumn: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
getDirtyData: vi.fn(),
|
||||
};
|
||||
|
||||
commandService.emit(SetFeatureCalculationMutation.id, {
|
||||
featureId: 'feature-1',
|
||||
calculationParam,
|
||||
});
|
||||
expect(featureCalculationManagerService.register).toHaveBeenCalledWith('unit-1', 'sheet-1', 'feature-1', calculationParam);
|
||||
|
||||
commandService.emit(RemoveFeatureCalculationMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
featureIds: ['feature-1'],
|
||||
});
|
||||
expect(featureCalculationManagerService.remove).toHaveBeenCalledWith('unit-1', 'sheet-1', ['feature-1']);
|
||||
});
|
||||
|
||||
it('ignores null params and unrelated commands', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const featureCalculationManagerService = {
|
||||
register: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetFeatureCalculationController(commandService as never, featureCalculationManagerService as never);
|
||||
|
||||
commandService.emit(SetFeatureCalculationMutation.id, null);
|
||||
commandService.emit(RemoveFeatureCalculationMutation.id, null);
|
||||
commandService.emit('unknown.command', {});
|
||||
|
||||
expect(featureCalculationManagerService.register).not.toHaveBeenCalled();
|
||||
expect(featureCalculationManagerService.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { RemoveOtherFormulaMutation, SetOtherFormulaMutation } from '../../commands/mutations/set-other-formula.mutation';
|
||||
import { SetOtherFormulaController } from '../set-other-formula.controller';
|
||||
|
||||
interface ICommandServiceMock {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => { dispose: () => void };
|
||||
emit: (id: string, params?: unknown) => void;
|
||||
}
|
||||
|
||||
function createCommandServiceMock(): ICommandServiceMock {
|
||||
const callbacks = new Set<(commandInfo: ICommandInfo) => void>();
|
||||
return {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => {
|
||||
callbacks.add(callback);
|
||||
return {
|
||||
dispose: () => callbacks.delete(callback),
|
||||
};
|
||||
},
|
||||
emit: (id: string, params?: unknown) => {
|
||||
callbacks.forEach((callback) => callback({ id, params } as ICommandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('SetOtherFormulaController', () => {
|
||||
it('should register and remove other-formula configs when commands are executed', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const otherFormulaManagerService = {
|
||||
batchRegister: vi.fn(),
|
||||
batchRemove: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetOtherFormulaController(
|
||||
commandService as never,
|
||||
otherFormulaManagerService as never,
|
||||
{} as never
|
||||
);
|
||||
|
||||
const formulaMap = {
|
||||
f1: { f: '=A1' },
|
||||
f2: { f: '=B2' },
|
||||
};
|
||||
|
||||
commandService.emit(SetOtherFormulaMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
formulaMap,
|
||||
});
|
||||
|
||||
expect(otherFormulaManagerService.batchRegister).toHaveBeenCalledWith({
|
||||
'unit-1': {
|
||||
'sheet-1': formulaMap,
|
||||
},
|
||||
});
|
||||
|
||||
commandService.emit(RemoveOtherFormulaMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
formulaIdList: ['f1', 'f2'],
|
||||
});
|
||||
|
||||
expect(otherFormulaManagerService.batchRemove).toHaveBeenCalledWith({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
f1: true,
|
||||
f2: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore null params and unrelated commands', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const otherFormulaManagerService = {
|
||||
batchRegister: vi.fn(),
|
||||
batchRemove: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetOtherFormulaController(
|
||||
commandService as never,
|
||||
otherFormulaManagerService as never,
|
||||
{} as never
|
||||
);
|
||||
|
||||
commandService.emit(SetOtherFormulaMutation.id, null);
|
||||
commandService.emit(RemoveOtherFormulaMutation.id, null);
|
||||
commandService.emit('unknown.command', {});
|
||||
|
||||
expect(otherFormulaManagerService.batchRegister).not.toHaveBeenCalled();
|
||||
expect(otherFormulaManagerService.batchRemove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '../../commands/mutations/set-defined-name.mutation';
|
||||
import { SetSuperTableOptionMutation } from '../../commands/mutations/set-super-table.mutation';
|
||||
import { SetSuperTableController } from '../set-super-table.controller';
|
||||
|
||||
interface ICommandServiceMock {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => { dispose: () => void };
|
||||
emit: (id: string, params?: unknown) => void;
|
||||
}
|
||||
|
||||
function createCommandServiceMock(): ICommandServiceMock {
|
||||
const callbacks = new Set<(commandInfo: ICommandInfo) => void>();
|
||||
return {
|
||||
onCommandExecuted: (callback: (commandInfo: ICommandInfo) => void) => {
|
||||
callbacks.add(callback);
|
||||
return {
|
||||
dispose: () => callbacks.delete(callback),
|
||||
};
|
||||
},
|
||||
emit: (id: string, params?: unknown) => {
|
||||
callbacks.forEach((callback) => callback({ id, params } as ICommandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('SetSuperTableController', () => {
|
||||
it('should dispatch table register/remove/option commands', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const superTableService = {
|
||||
registerTable: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
registerTableOptionMap: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetSuperTableController(commandService as never, superTableService as never);
|
||||
|
||||
const reference = {
|
||||
tableId: 'table-id',
|
||||
range: { startRow: 0, endRow: 10, startColumn: 0, endColumn: 2 },
|
||||
};
|
||||
|
||||
commandService.emit(SetDefinedNameMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
tableName: 'Table1',
|
||||
reference,
|
||||
});
|
||||
expect(superTableService.registerTable).toHaveBeenCalledWith('unit-1', 'Table1', reference);
|
||||
|
||||
commandService.emit(RemoveDefinedNameMutation.id, {
|
||||
unitId: 'unit-1',
|
||||
tableName: 'Table1',
|
||||
});
|
||||
expect(superTableService.remove).toHaveBeenCalledWith('unit-1', 'Table1');
|
||||
|
||||
const tableOption = {
|
||||
Table1: { totalRow: true },
|
||||
};
|
||||
commandService.emit(SetSuperTableOptionMutation.id, {
|
||||
tableOption,
|
||||
tableOptionType: 'normal',
|
||||
});
|
||||
expect(superTableService.registerTableOptionMap).toHaveBeenCalledWith(tableOption, 'normal');
|
||||
});
|
||||
|
||||
it('should ignore null params and unrelated commands', () => {
|
||||
const commandService = createCommandServiceMock();
|
||||
const superTableService = {
|
||||
registerTable: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
registerTableOptionMap: vi.fn(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new SetSuperTableController(commandService as never, superTableService as never);
|
||||
|
||||
commandService.emit(SetDefinedNameMutation.id, null);
|
||||
commandService.emit(RemoveDefinedNameMutation.id, null);
|
||||
commandService.emit(SetSuperTableOptionMutation.id, null);
|
||||
commandService.emit('unknown.command');
|
||||
|
||||
expect(superTableService.registerTable).not.toHaveBeenCalled();
|
||||
expect(superTableService.remove).not.toHaveBeenCalled();
|
||||
expect(superTableService.registerTableOptionMap).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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 { IDirtyConversionManagerParams } from '../../services/active-dirty-manager.service';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { RemoveSuperTableMutation, SetSuperTableMutation } from '../../commands/mutations/set-super-table.mutation';
|
||||
import { SuperTableActiveDirtyController } from '../super-table-active-dirty.controller';
|
||||
|
||||
describe('SuperTableActiveDirtyController', () => {
|
||||
it('invalidates both the old and new table name when a registration changes', () => {
|
||||
const conversions = new Map<string, IDirtyConversionManagerParams>();
|
||||
const activeDirtyManagerService = {
|
||||
register: vi.fn((id: string, conversion: IDirtyConversionManagerParams) => conversions.set(id, conversion)),
|
||||
remove: vi.fn((id: string) => conversions.delete(id)),
|
||||
};
|
||||
const controller = new SuperTableActiveDirtyController(activeDirtyManagerService as never);
|
||||
|
||||
const dirtyData = conversions.get(SetSuperTableMutation.id)?.getDirtyData({
|
||||
id: SetSuperTableMutation.id,
|
||||
params: {
|
||||
unitId: 'base-1',
|
||||
tableName: 'Costs',
|
||||
oldTableName: 'Pricing',
|
||||
reference: {
|
||||
sheetId: 'table-1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 9, endColumn: 2 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dirtyData).toEqual({
|
||||
dirtyRanges: [{
|
||||
unitId: 'base-1',
|
||||
sheetId: 'table-1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 9, endColumn: 2 },
|
||||
}],
|
||||
dirtySuperTableMap: { 'base-1': { Costs: '1', Pricing: '1' } },
|
||||
clearDependencyTreeCache: { 'base-1': { 'table-1': '1' } },
|
||||
});
|
||||
|
||||
expect(conversions.get(RemoveSuperTableMutation.id)?.getDirtyData({
|
||||
id: RemoveSuperTableMutation.id,
|
||||
params: {
|
||||
unitId: 'base-1',
|
||||
tableName: 'Pricing',
|
||||
reference: {
|
||||
sheetId: 'table-1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 9, endColumn: 2 },
|
||||
},
|
||||
},
|
||||
})).toEqual({
|
||||
dirtyRanges: [{
|
||||
unitId: 'base-1',
|
||||
sheetId: 'table-1',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 9, endColumn: 2 },
|
||||
}],
|
||||
dirtySuperTableMap: { 'base-1': { Pricing: '1' } },
|
||||
clearDependencyTreeCache: { 'base-1': { 'table-1': '1' } },
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
expect(conversions.size).toBe(0);
|
||||
});
|
||||
});
|
||||
350
packages/engine-formula/src/controllers/calculate.controller.ts
Normal file
350
packages/engine-formula/src/controllers/calculate.controller.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type { ISetArrayFormulaDataMutationParams } from '../commands/mutations/set-array-formula-data.mutation';
|
||||
import type {
|
||||
ISetFormulaCalculationStartMutation,
|
||||
ISetFormulaDependencyCalculationMutation,
|
||||
ISetFormulaStringBatchCalculationMutation,
|
||||
ISetQueryFormulaDependencyAllMutation,
|
||||
ISetQueryFormulaDependencyMutation,
|
||||
} from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import type {
|
||||
IFormulaDependencyTreeJson,
|
||||
IFormulaDependentsAndInRangeResults,
|
||||
} from '../engine/dependency/dependency-tree';
|
||||
import type { IFormulaDirtyData } from '../services/current-data.service';
|
||||
import type { IAllRuntimeData } from '../services/runtime.service';
|
||||
import { Disposable, ICommandService, Inject } from '@univerjs/core';
|
||||
import { convertRuntimeToUnitData } from '../basics/runtime';
|
||||
import { SetArrayFormulaDataMutation } from '../commands/mutations/set-array-formula-data.mutation';
|
||||
import {
|
||||
SetCellFormulaDependencyCalculationMutation,
|
||||
SetCellFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetFormulaCalculationStopMutation,
|
||||
SetFormulaDependencyCalculationMutation,
|
||||
SetFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaStringBatchCalculationMutation,
|
||||
SetFormulaStringBatchCalculationResultMutation,
|
||||
SetQueryFormulaDependencyAllMutation,
|
||||
SetQueryFormulaDependencyAllResultMutation,
|
||||
SetQueryFormulaDependencyMutation,
|
||||
SetQueryFormulaDependencyResultMutation,
|
||||
} from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import { SetImageFormulaDataMutation } from '../commands/mutations/set-image-formula-data.mutation';
|
||||
import { DEFAULT_CYCLE_REFERENCE_COUNT } from '../config/config';
|
||||
import { FormulaDataModel } from '../models/formula-data.model';
|
||||
import { ICalculateFormulaService } from '../services/calculate-formula.service';
|
||||
import { FormulaExecutedStateType } from '../services/runtime.service';
|
||||
|
||||
export class CalculateController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ICalculateFormulaService private readonly _calculateFormulaService: ICalculateFormulaService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._commandExecutedListener();
|
||||
this._initialExecuteFormulaListener();
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetFormulaCalculationStopMutation.id) {
|
||||
this._calculateFormulaService.stopFormulaExecution();
|
||||
} else if (command.id === SetFormulaCalculationStartMutation.id) {
|
||||
this._calculate(command.params as ISetFormulaCalculationStartMutation);
|
||||
} else if (command.id === SetArrayFormulaDataMutation.id) {
|
||||
const params = command.params as ISetArrayFormulaDataMutationParams;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { arrayFormulaRange, arrayFormulaCellData } = params;
|
||||
// TODO@Dushusir: Merge the array formula data into the formulaDataModel
|
||||
this._formulaDataModel.setArrayFormulaRange(arrayFormulaRange);
|
||||
this._formulaDataModel.setArrayFormulaCellData(arrayFormulaCellData);
|
||||
} else if (command.id === SetFormulaStringBatchCalculationMutation.id) {
|
||||
this._calculateFormulaString(command.params as ISetFormulaStringBatchCalculationMutation);
|
||||
} else if (command.id === SetFormulaDependencyCalculationMutation.id) {
|
||||
this._generateAllDependencyTreeJson();
|
||||
} else if (command.id === SetCellFormulaDependencyCalculationMutation.id) {
|
||||
this._generateCellDependencyTreeJson(command.params as ISetFormulaDependencyCalculationMutation);
|
||||
} else if (command.id === SetQueryFormulaDependencyMutation.id) {
|
||||
const params = command.params as ISetQueryFormulaDependencyMutation;
|
||||
this._queryFormulaDependencyJson(params);
|
||||
} else if (command.id === SetQueryFormulaDependencyAllMutation.id) {
|
||||
const params = command.params as ISetQueryFormulaDependencyAllMutation;
|
||||
this._queryFormulaDependencyAllJson(params);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async _calculate(formulaDirtyData: Partial<IFormulaDirtyData>) {
|
||||
const {
|
||||
forceCalculation: forceCalculate = false,
|
||||
dirtyRanges = [],
|
||||
dirtyNameMap = {},
|
||||
dirtyDefinedNameMap = {},
|
||||
dirtySuperTableMap = {},
|
||||
dirtyUnitFeatureMap = {},
|
||||
dirtyUnitOtherFormulaMap = {},
|
||||
clearDependencyTreeCache = {},
|
||||
maxIteration = DEFAULT_CYCLE_REFERENCE_COUNT,
|
||||
rowData,
|
||||
externalReferences,
|
||||
isCalculateTreeModel = false,
|
||||
} = formulaDirtyData;
|
||||
|
||||
const formulaData = this._formulaDataModel.getFormulaData();
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData();
|
||||
// array formula range is used to check whether the newly added array formula conflicts with the existing array formula
|
||||
const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange();
|
||||
|
||||
this._calculateFormulaService.execute({
|
||||
formulaData,
|
||||
arrayFormulaCellData,
|
||||
arrayFormulaRange,
|
||||
forceCalculate,
|
||||
dirtyRanges,
|
||||
dirtyNameMap,
|
||||
dirtyDefinedNameMap,
|
||||
dirtySuperTableMap,
|
||||
dirtyUnitFeatureMap,
|
||||
dirtyUnitOtherFormulaMap,
|
||||
clearDependencyTreeCache,
|
||||
maxIteration,
|
||||
isCalculateTreeModel,
|
||||
rowData,
|
||||
externalReferences,
|
||||
});
|
||||
}
|
||||
|
||||
private async _queryFormulaDependencyJson(param: ISetQueryFormulaDependencyMutation) {
|
||||
const { unitRanges, isInRange } = param;
|
||||
let result: IFormulaDependencyTreeJson[] = [];
|
||||
if (isInRange) {
|
||||
result = await this._calculateFormulaService.getInRangeFormulas(unitRanges);
|
||||
} else {
|
||||
result = await this._calculateFormulaService.getRangeDependents(unitRanges);
|
||||
}
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetQueryFormulaDependencyResultMutation.id,
|
||||
{
|
||||
result,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _queryFormulaDependencyAllJson(param: ISetQueryFormulaDependencyAllMutation) {
|
||||
const { unitRanges } = param;
|
||||
const result: IFormulaDependentsAndInRangeResults = await this._calculateFormulaService.getDependentsAndInRangeFormulas(unitRanges);
|
||||
this._commandService.executeCommand(
|
||||
SetQueryFormulaDependencyAllResultMutation.id,
|
||||
{
|
||||
result,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _generateAllDependencyTreeJson() {
|
||||
const result = await this._calculateFormulaService.getAllDependencyJson();
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetFormulaDependencyCalculationResultMutation.id,
|
||||
{
|
||||
result,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _generateCellDependencyTreeJson(param: ISetFormulaDependencyCalculationMutation) {
|
||||
const { unitId, sheetId, row, column } = param;
|
||||
const result = await this._calculateFormulaService.getCellDependencyJson(unitId, sheetId, row, column);
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetCellFormulaDependencyCalculationResultMutation.id,
|
||||
{
|
||||
result,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _calculateFormulaString(param: ISetFormulaStringBatchCalculationMutation) {
|
||||
const { formulas } = param;
|
||||
const result = await this._calculateFormulaService.executeFormulas(
|
||||
formulas
|
||||
);
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetFormulaStringBatchCalculationResultMutation.id,
|
||||
{
|
||||
result,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Notification
|
||||
private _initialExecuteFormulaListener() {
|
||||
/**
|
||||
* Assignment operation after formula calculation.
|
||||
*/
|
||||
this._calculateFormulaService.executionCompleteListener$.subscribe((data) => {
|
||||
const functionsExecutedState = data.functionsExecutedState;
|
||||
switch (functionsExecutedState) {
|
||||
case FormulaExecutedStateType.NOT_EXECUTED:
|
||||
this._applyTreeResult(data);
|
||||
break;
|
||||
case FormulaExecutedStateType.STOP_EXECUTION:
|
||||
break;
|
||||
case FormulaExecutedStateType.SUCCESS:
|
||||
this._applyResult(data);
|
||||
break;
|
||||
case FormulaExecutedStateType.INITIAL:
|
||||
break;
|
||||
}
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetFormulaCalculationNotificationMutation.id,
|
||||
{
|
||||
functionsExecutedState,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Assignment operation after formula calculation.
|
||||
*/
|
||||
this._calculateFormulaService.executionInProgressListener$.subscribe((data) => {
|
||||
this._commandService.executeCommand(
|
||||
SetFormulaCalculationNotificationMutation.id,
|
||||
{
|
||||
stageInfo: data,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async _applyTreeResult(data: IAllRuntimeData) {
|
||||
const { dependencyTreeModelData } = data;
|
||||
if (dependencyTreeModelData.length > 0) {
|
||||
this._commandService.executeCommand(
|
||||
SetFormulaDependencyCalculationResultMutation.id,
|
||||
{
|
||||
result: dependencyTreeModelData,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _applyResult(data: IAllRuntimeData) {
|
||||
const {
|
||||
unitData,
|
||||
unitOtherData,
|
||||
arrayFormulaRange,
|
||||
arrayFormulaCellData,
|
||||
clearArrayFormulaCellData,
|
||||
arrayFormulaEmbedded,
|
||||
imageFormulaData,
|
||||
} = data;
|
||||
|
||||
if (!unitData) {
|
||||
this._applyTreeResult(data);
|
||||
console.error('No sheetData from Formula Engine!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (arrayFormulaRange || arrayFormulaEmbedded) {
|
||||
this._formulaDataModel.clearPreviousArrayFormulaCellData(clearArrayFormulaCellData);
|
||||
this._formulaDataModel.mergeArrayFormulaCellData(arrayFormulaCellData);
|
||||
this._formulaDataModel.mergeArrayFormulaRange(arrayFormulaRange);
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetArrayFormulaDataMutation.id,
|
||||
{
|
||||
arrayFormulaRange: this._formulaDataModel.getArrayFormulaRange(),
|
||||
arrayFormulaCellData: this._formulaDataModel.getArrayFormulaCellData(),
|
||||
arrayFormulaEmbedded,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// handle image formula data
|
||||
if (imageFormulaData && imageFormulaData.length > 0) {
|
||||
this._commandService.executeCommand(
|
||||
SetImageFormulaDataMutation.id,
|
||||
{
|
||||
imageFormulaData,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this._applyTreeResult(data);
|
||||
|
||||
this._commandService.executeCommand(
|
||||
SetFormulaCalculationResultMutation.id,
|
||||
{
|
||||
unitData: convertRuntimeToUnitData(unitData),
|
||||
unitOtherData,
|
||||
},
|
||||
{
|
||||
onlyLocal: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type { ISetFormulaCalculationNotificationMutation } from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import { Disposable, DisposableCollection, ICommandService, Inject } from '@univerjs/core';
|
||||
import { BehaviorSubject, distinctUntilChanged, Observable, shareReplay } from 'rxjs';
|
||||
import { SetFormulaCalculationNotificationMutation } from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import { GlobalComputingStatusService } from '../services/global-computing-status.service';
|
||||
import { FormulaExecuteStageType } from '../services/runtime.service';
|
||||
|
||||
// TODO@wzhudev: move logics in Facade to this place.
|
||||
|
||||
/**
|
||||
* This controller monitors the calculating status of the formula engine,
|
||||
* and expose some internal status to the outside.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export class ComputingStatusReporterController extends Disposable {
|
||||
private _computingCompleted$ = new Observable<boolean>((observe) => {
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id !== SetFormulaCalculationNotificationMutation.id) return;
|
||||
|
||||
const params = command.params as ISetFormulaCalculationNotificationMutation;
|
||||
if (params.stageInfo) {
|
||||
return observe.next(
|
||||
params.stageInfo.stage === FormulaExecuteStageType.IDLE
|
||||
|| params.stageInfo.stage === FormulaExecuteStageType.CALCULATION_COMPLETED
|
||||
);
|
||||
}
|
||||
});
|
||||
}).pipe(
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true })
|
||||
);
|
||||
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(GlobalComputingStatusService) private readonly _globalComputingSrv: GlobalComputingStatusService
|
||||
) {
|
||||
super();
|
||||
|
||||
const disposables = new DisposableCollection();
|
||||
const subject = new BehaviorSubject(true);
|
||||
disposables.add(this._globalComputingSrv.pushComputingStatusSubject(subject));
|
||||
disposables.add(this._computingCompleted$.subscribe((completed) => subject.next(completed)));
|
||||
disposables.add(() => subject.complete());
|
||||
this.disposeWithMe(disposables);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type {
|
||||
ISetFormulaCalculationNotificationMutation,
|
||||
ISetFormulaCalculationResultMutation,
|
||||
} from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
} from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import {
|
||||
FormulaCalculationSessionService,
|
||||
FormulaResultApplicationType,
|
||||
} from '../services/formula/formula-calculation-session.service';
|
||||
import { RegisterOtherFormulaService } from '../services/register-other-formula.service';
|
||||
|
||||
function isFormulaCalculationNotification(
|
||||
params: object | undefined
|
||||
): params is ISetFormulaCalculationNotificationMutation {
|
||||
return params != null;
|
||||
}
|
||||
|
||||
function isFormulaCalculationResult(
|
||||
params: object | undefined
|
||||
): params is ISetFormulaCalculationResultMutation {
|
||||
return params != null && 'unitData' in params && 'unitOtherData' in params;
|
||||
}
|
||||
|
||||
export class FormulaCalculationSessionController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(FormulaCalculationSessionService) private readonly _sessionService: FormulaCalculationSessionService,
|
||||
@Inject(RegisterOtherFormulaService) private readonly _registerOtherFormulaService: RegisterOtherFormulaService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._sessionService.initialize();
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this.disposeWithMe(this._registerOtherFormulaService.otherFormulaResultApplied$.subscribe((result) => {
|
||||
this._sessionService.markResultApplied(FormulaResultApplicationType.OTHER_FORMULA, result);
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetFormulaCalculationStartMutation.id) {
|
||||
this._sessionService.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.id === SetFormulaCalculationNotificationMutation.id) {
|
||||
if (isFormulaCalculationNotification(command.params)) {
|
||||
this._handleNotification(command.params);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.id === SetFormulaCalculationResultMutation.id) {
|
||||
if (isFormulaCalculationResult(command.params)) {
|
||||
this._handleResult(command.params);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _handleNotification(params: ISetFormulaCalculationNotificationMutation): void {
|
||||
if (params.stageInfo != null) {
|
||||
this._sessionService.updateProgress(params.stageInfo);
|
||||
}
|
||||
|
||||
if (params.functionsExecutedState !== undefined) {
|
||||
this._sessionService.markCompleted(params.functionsExecutedState);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleResult(params: ISetFormulaCalculationResultMutation): void {
|
||||
const pendingApplications: FormulaResultApplicationType[] = [];
|
||||
|
||||
if (this._hasSheetResultToApply(params)) {
|
||||
pendingApplications.push(FormulaResultApplicationType.SHEET);
|
||||
}
|
||||
|
||||
if (this._hasBaseResultToApply(params)) {
|
||||
pendingApplications.push(FormulaResultApplicationType.BASE);
|
||||
}
|
||||
|
||||
if (this._hasOtherFormulaResultToApply(params)) {
|
||||
pendingApplications.push(FormulaResultApplicationType.OTHER_FORMULA);
|
||||
}
|
||||
|
||||
this._sessionService.markResultEmitted(params, pendingApplications);
|
||||
}
|
||||
|
||||
private _hasSheetResultToApply(result: ISetFormulaCalculationResultMutation): boolean {
|
||||
return this._hasUnitResultToApply(result, UniverInstanceType.UNIVER_SHEET);
|
||||
}
|
||||
|
||||
private _hasBaseResultToApply(result: ISetFormulaCalculationResultMutation): boolean {
|
||||
return this._hasUnitResultToApply(result, UniverInstanceType.UNIVER_BASE);
|
||||
}
|
||||
|
||||
private _hasUnitResultToApply(
|
||||
result: ISetFormulaCalculationResultMutation,
|
||||
unitType: UniverInstanceType
|
||||
): boolean {
|
||||
// Only registered host units have a matching result applicator. Unknown units must not keep the session pending.
|
||||
return Object.entries(result.unitData).some(([unitId, sheetData]) =>
|
||||
this._univerInstanceService.getUnit(unitId, unitType) != null
|
||||
&& sheetData != null
|
||||
&& Object.values(sheetData).some((cellData) => cellData != null)
|
||||
);
|
||||
}
|
||||
|
||||
private _hasOtherFormulaResultToApply(result: ISetFormulaCalculationResultMutation): boolean {
|
||||
return Object.values(result.unitOtherData).some((subUnitData) =>
|
||||
subUnitData != null && Object.values(subUnitData).some((formulaData) =>
|
||||
formulaData != null && Object.keys(formulaData).length > 0
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 { Disposable, Inject, isNodeEnv, LifecycleService, LifecycleStages } from '@univerjs/core';
|
||||
import { filter, take } from 'rxjs';
|
||||
import { FormulaCalculationTriggerService } from '../services/formula-calculation-trigger.service';
|
||||
|
||||
export class FormulaCalculationTriggerController extends Disposable {
|
||||
constructor(
|
||||
@Inject(FormulaCalculationTriggerService) formulaCalculationTriggerService: FormulaCalculationTriggerService,
|
||||
@Inject(LifecycleService) lifecycleService: LifecycleService
|
||||
) {
|
||||
super();
|
||||
|
||||
const start = () => formulaCalculationTriggerService.start();
|
||||
|
||||
if (isNodeEnv()) {
|
||||
start();
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeWithMe(lifecycleService.lifecycle$.pipe(
|
||||
filter((stage) => stage >= LifecycleStages.Rendered),
|
||||
take(1)
|
||||
).subscribe(start));
|
||||
}
|
||||
}
|
||||
148
packages/engine-formula/src/controllers/formula.controller.ts
Normal file
148
packages/engine-formula/src/controllers/formula.controller.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 { Ctor } from '@univerjs/core';
|
||||
import type { IFunctionNames } from '../basics/function';
|
||||
import type { IUniverEngineFormulaConfig } from '../config/config';
|
||||
import type { BaseFunction } from '../functions/base-function';
|
||||
import { Disposable, ICommandService, IConfigService, Inject, Optional } from '@univerjs/core';
|
||||
import { DataSyncPrimaryController } from '@univerjs/rpc';
|
||||
import { OtherFormulaMarkDirty } from '../commands/mutations/formula.mutation';
|
||||
import { RegisterFunctionMutation } from '../commands/mutations/register-function.mutation';
|
||||
import { SetArrayFormulaDataMutation } from '../commands/mutations/set-array-formula-data.mutation';
|
||||
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '../commands/mutations/set-defined-name.mutation';
|
||||
import { RemoveFeatureCalculationMutation, SetFeatureCalculationMutation } from '../commands/mutations/set-feature-calculation.mutation';
|
||||
import {
|
||||
SetCellFormulaDependencyCalculationMutation,
|
||||
SetCellFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetFormulaCalculationStopMutation,
|
||||
SetFormulaDependencyCalculationMutation,
|
||||
SetFormulaDependencyCalculationResultMutation,
|
||||
SetFormulaStringBatchCalculationMutation,
|
||||
SetFormulaStringBatchCalculationResultMutation,
|
||||
SetQueryFormulaDependencyAllMutation,
|
||||
SetQueryFormulaDependencyAllResultMutation,
|
||||
SetQueryFormulaDependencyMutation,
|
||||
SetQueryFormulaDependencyResultMutation,
|
||||
SetTriggerFormulaCalculationStartMutation,
|
||||
} from '../commands/mutations/set-formula-calculation.mutation';
|
||||
import { SetFormulaDataMutation } from '../commands/mutations/set-formula-data.mutation';
|
||||
import { SetImageFormulaDataMutation } from '../commands/mutations/set-image-formula-data.mutation';
|
||||
import { RemoveOtherFormulaMutation, SetOtherFormulaMutation } from '../commands/mutations/set-other-formula.mutation';
|
||||
import { RemoveSuperTableMutation, SetSuperTableMutation, SetSuperTableOptionMutation } from '../commands/mutations/set-super-table.mutation';
|
||||
import { ENGINE_FORMULA_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { ALL_IMPLEMENTED_FUNCTIONS } from '../functions';
|
||||
import { IFunctionService } from '../services/function.service';
|
||||
import { RegisterOtherFormulaService } from '../services/register-other-formula.service';
|
||||
|
||||
type FormulaDataSyncController = Pick<
|
||||
DataSyncPrimaryController,
|
||||
'registerSyncingMutations' | 'syncUnitMutations' | 'waitForPendingMutations'
|
||||
>;
|
||||
|
||||
export class FormulaController extends Disposable {
|
||||
private readonly _syncedOtherFormulaUnits = new Set<string>();
|
||||
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@Optional(DataSyncPrimaryController) private readonly _dataSyncPrimaryController: FormulaDataSyncController | undefined,
|
||||
@Inject(RegisterOtherFormulaService) private readonly _registerOtherFormulaService: RegisterOtherFormulaService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._registerCommands();
|
||||
this._registerFunctions();
|
||||
this._syncOtherFormulaUnits();
|
||||
}
|
||||
|
||||
private _syncOtherFormulaUnits(): void {
|
||||
const dataSyncPrimaryController = this._dataSyncPrimaryController;
|
||||
if (!dataSyncPrimaryController) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeWithMe(this._registerOtherFormulaService.setMutationSyncHandler((unitId) => {
|
||||
if (!this._syncedOtherFormulaUnits.has(unitId)) {
|
||||
this._syncedOtherFormulaUnits.add(unitId);
|
||||
this.disposeWithMe(dataSyncPrimaryController.syncUnitMutations(unitId));
|
||||
}
|
||||
return () => dataSyncPrimaryController.waitForPendingMutations();
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
[
|
||||
SetFormulaDataMutation,
|
||||
SetArrayFormulaDataMutation,
|
||||
SetImageFormulaDataMutation,
|
||||
SetFormulaCalculationStartMutation,
|
||||
SetTriggerFormulaCalculationStartMutation,
|
||||
SetFormulaStringBatchCalculationMutation,
|
||||
SetFormulaStringBatchCalculationResultMutation,
|
||||
SetQueryFormulaDependencyMutation,
|
||||
SetQueryFormulaDependencyResultMutation,
|
||||
SetFormulaCalculationStopMutation,
|
||||
SetFormulaCalculationNotificationMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
SetQueryFormulaDependencyAllMutation,
|
||||
SetQueryFormulaDependencyAllResultMutation,
|
||||
|
||||
SetFormulaDependencyCalculationMutation,
|
||||
SetFormulaDependencyCalculationResultMutation,
|
||||
SetCellFormulaDependencyCalculationMutation,
|
||||
SetCellFormulaDependencyCalculationResultMutation,
|
||||
|
||||
SetDefinedNameMutation,
|
||||
RemoveDefinedNameMutation,
|
||||
SetFeatureCalculationMutation,
|
||||
RemoveFeatureCalculationMutation,
|
||||
|
||||
OtherFormulaMarkDirty,
|
||||
SetOtherFormulaMutation,
|
||||
RemoveOtherFormulaMutation,
|
||||
SetSuperTableMutation,
|
||||
RemoveSuperTableMutation,
|
||||
SetSuperTableOptionMutation,
|
||||
RegisterFunctionMutation,
|
||||
].forEach((mutation) => {
|
||||
this._commandService.registerCommand(mutation);
|
||||
this._dataSyncPrimaryController?.registerSyncingMutations(mutation);
|
||||
});
|
||||
}
|
||||
|
||||
private _registerFunctions() {
|
||||
const config = this._configService.getConfig<IUniverEngineFormulaConfig>(ENGINE_FORMULA_PLUGIN_CONFIG_KEY);
|
||||
|
||||
const functions: BaseFunction[] = ALL_IMPLEMENTED_FUNCTIONS.concat(config?.function ?? [])
|
||||
.map((registerObject) => {
|
||||
const Func = registerObject[0] as Ctor<BaseFunction>;
|
||||
const name = registerObject[1] as IFunctionNames;
|
||||
|
||||
return new Func(name);
|
||||
});
|
||||
|
||||
this._functionService.registerExecutors(...functions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type { ISetDefinedNameMutationParam } from '../commands/mutations/set-defined-name.mutation';
|
||||
|
||||
import type {
|
||||
IRemoveFeatureCalculationMutationParam,
|
||||
ISetFeatureCalculationMutation,
|
||||
} from '../commands/mutations/set-feature-calculation.mutation';
|
||||
import type { ISetFormulaDataMutationParams } from '../commands/mutations/set-formula-data.mutation';
|
||||
import type { IRemoveOtherFormulaMutationParams, ISetOtherFormulaMutationParams } from '../commands/mutations/set-other-formula.mutation';
|
||||
import { Disposable, ICommandService, ObjectMatrix } from '@univerjs/core';
|
||||
import { SetDefinedNameMutation } from '../commands/mutations/set-defined-name.mutation';
|
||||
import {
|
||||
RemoveFeatureCalculationMutation,
|
||||
SetFeatureCalculationMutation,
|
||||
} from '../commands/mutations/set-feature-calculation.mutation';
|
||||
import { SetFormulaDataMutation } from '../commands/mutations/set-formula-data.mutation';
|
||||
import { RemoveOtherFormulaMutation, SetOtherFormulaMutation } from '../commands/mutations/set-other-formula.mutation';
|
||||
import { IDependencyManagerService } from '../services/dependency-manager.service';
|
||||
import { IFeatureCalculationManagerService } from '../services/feature-calculation-manager.service';
|
||||
|
||||
export class SetDependencyController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IFeatureCalculationManagerService
|
||||
@IDependencyManagerService private readonly _dependencyManagerService: IDependencyManagerService,
|
||||
@IFeatureCalculationManagerService private readonly _featureCalculationManagerService: IFeatureCalculationManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._commandExecutedListener();
|
||||
|
||||
this._featureCalculationManagerServiceListener();
|
||||
}
|
||||
|
||||
private _featureCalculationManagerServiceListener() {
|
||||
this.disposeWithMe(
|
||||
this._featureCalculationManagerService.onChanged$.subscribe((params) => {
|
||||
const { unitId, subUnitId, featureIds } = params;
|
||||
this._dependencyManagerService.removeFeatureFormulaDependency(unitId, subUnitId, featureIds);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === RemoveFeatureCalculationMutation.id) {
|
||||
const params = command.params as IRemoveFeatureCalculationMutationParam;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { featureIds, unitId, subUnitId } = params;
|
||||
this._dependencyManagerService.removeFeatureFormulaDependency(unitId, subUnitId, featureIds);
|
||||
} else if (command.id === SetFeatureCalculationMutation.id) {
|
||||
const params = command.params as ISetFeatureCalculationMutation;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { featureId, calculationParam } = params;
|
||||
const { unitId, subUnitId } = calculationParam;
|
||||
this._dependencyManagerService.removeFeatureFormulaDependency(unitId, subUnitId, [featureId]);
|
||||
} else if (command.id === RemoveOtherFormulaMutation.id) {
|
||||
const params = command.params as IRemoveOtherFormulaMutationParams;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dependencyManagerService.removeOtherFormulaDependency(params.unitId, params.subUnitId, params.formulaIdList);
|
||||
} else if (command.id === SetOtherFormulaMutation.id) {
|
||||
const params = command.params as ISetOtherFormulaMutationParams;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formulaMap = params.formulaMap;
|
||||
const formulaIdList: string[] = [];
|
||||
Object.keys(formulaMap).forEach((formulaId) => {
|
||||
formulaIdList.push(formulaId);
|
||||
});
|
||||
this._dependencyManagerService.removeOtherFormulaDependency(params.unitId, params.subUnitId, formulaIdList);
|
||||
} else if (command.id === SetFormulaDataMutation.id) {
|
||||
const formulaData = (command.params as ISetFormulaDataMutationParams).formulaData;
|
||||
Object.keys(formulaData).forEach((unitId) => {
|
||||
const unitFormulaData = formulaData[unitId];
|
||||
// undefined means do nothing
|
||||
if (unitFormulaData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// null means clear all formula data with the unitId
|
||||
if (unitFormulaData === null) {
|
||||
this._dependencyManagerService.clearFormulaDependency(unitId);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(unitFormulaData).forEach((subUnitId) => {
|
||||
const formulaDataItem = unitFormulaData[subUnitId];
|
||||
|
||||
if (formulaDataItem === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (formulaDataItem === null) {
|
||||
this._dependencyManagerService.clearFormulaDependency(unitId, subUnitId);
|
||||
return true;
|
||||
}
|
||||
|
||||
new ObjectMatrix(formulaDataItem).forValue((row, column) => {
|
||||
this._dependencyManagerService.removeFormulaDependency(unitId, subUnitId, row, column);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (command.id === SetDefinedNameMutation.id) {
|
||||
this._handleSetDefinedName(command);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSetDefinedName(
|
||||
command: ICommandInfo
|
||||
): void {
|
||||
const params = command.params as ISetDefinedNameMutationParam;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, name } = params;
|
||||
|
||||
this._dependencyManagerService.removeFormulaDependencyByDefinedName(unitId, name);
|
||||
}
|
||||
}
|
||||
@@ -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 { ICommandInfo } from '@univerjs/core';
|
||||
import type {
|
||||
IRemoveFeatureCalculationMutationParam,
|
||||
ISetFeatureCalculationMutation,
|
||||
} from '../commands/mutations/set-feature-calculation.mutation';
|
||||
import { Disposable, ICommandService } from '@univerjs/core';
|
||||
import {
|
||||
RemoveFeatureCalculationMutation,
|
||||
SetFeatureCalculationMutation,
|
||||
} from '../commands/mutations/set-feature-calculation.mutation';
|
||||
import { IFeatureCalculationManagerService } from '../services/feature-calculation-manager.service';
|
||||
|
||||
export class SetFeatureCalculationController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IFeatureCalculationManagerService
|
||||
private readonly _featureCalculationManagerService: IFeatureCalculationManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._commandExecutedListener();
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetFeatureCalculationMutation.id) {
|
||||
const params = command.params as ISetFeatureCalculationMutation;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { featureId, calculationParam } = params;
|
||||
const { unitId, subUnitId } = calculationParam;
|
||||
this._featureCalculationManagerService.register(unitId, subUnitId, featureId, calculationParam);
|
||||
} else if (command.id === RemoveFeatureCalculationMutation.id) {
|
||||
const params = command.params as IRemoveFeatureCalculationMutationParam;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { featureIds, unitId, subUnitId } = params;
|
||||
this._featureCalculationManagerService.remove(unitId, subUnitId, featureIds);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type { IOtherFormulaData } from '../basics/common';
|
||||
import type {
|
||||
IRemoveOtherFormulaMutationParams,
|
||||
ISetOtherFormulaMutationParams,
|
||||
} from '../commands/mutations/set-other-formula.mutation';
|
||||
import { Disposable, ICommandService } from '@univerjs/core';
|
||||
import { RemoveOtherFormulaMutation, SetOtherFormulaMutation } from '../commands/mutations/set-other-formula.mutation';
|
||||
import { IDependencyManagerService } from '../services/dependency-manager.service';
|
||||
import {
|
||||
IOtherFormulaManagerService,
|
||||
} from '../services/other-formula-manager.service';
|
||||
|
||||
export class SetOtherFormulaController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IOtherFormulaManagerService private readonly _otherFormulaManagerService: IOtherFormulaManagerService,
|
||||
@IDependencyManagerService private readonly _dependencyManagerService: IDependencyManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._commandExecutedListener();
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetOtherFormulaMutation.id) {
|
||||
const params = command.params as ISetOtherFormulaMutationParams;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const config: IOtherFormulaData = { [params.unitId]: { [params.subUnitId]: params.formulaMap } };
|
||||
this._otherFormulaManagerService.batchRegister(config);
|
||||
} else if (command.id === RemoveOtherFormulaMutation.id) {
|
||||
const params = command.params as IRemoveOtherFormulaMutationParams;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const obj: Record<string, true> = {};
|
||||
params.formulaIdList.forEach((id) => obj[id] = true);
|
||||
const config = { [params.unitId]: { [params.subUnitId]: obj } };
|
||||
this._otherFormulaManagerService.batchRemove(config);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type {
|
||||
ISetSuperTableMutationParam,
|
||||
ISetSuperTableMutationSearchParam,
|
||||
} from '../commands/mutations/set-super-table.mutation';
|
||||
import type { ISuperTableOptionParam } from '../services/super-table.service';
|
||||
import { Disposable, ICommandService } from '@univerjs/core';
|
||||
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '../commands/mutations/set-defined-name.mutation';
|
||||
import { SetSuperTableOptionMutation } from '../commands/mutations/set-super-table.mutation';
|
||||
import { ISuperTableService } from '../services/super-table.service';
|
||||
|
||||
export class SetSuperTableController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ISuperTableService private readonly _superTableService: ISuperTableService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._commandExecutedListener();
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetDefinedNameMutation.id) {
|
||||
const params = command.params as ISetSuperTableMutationParam;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { unitId, tableName, reference } = params;
|
||||
this._superTableService.registerTable(unitId, tableName, reference);
|
||||
} else if (command.id === RemoveDefinedNameMutation.id) {
|
||||
const params = command.params as ISetSuperTableMutationSearchParam;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { unitId, tableName } = params;
|
||||
this._superTableService.remove(unitId, tableName);
|
||||
} else if (command.id === SetSuperTableOptionMutation.id) {
|
||||
const params = command.params as ISuperTableOptionParam;
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
const { tableOption, tableOptionType } = params;
|
||||
this._superTableService.registerTableOptionMap(tableOption, tableOptionType);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type { IRemoveSuperTableMutationParam, ISetSuperTableMutationParam } from '../commands/mutations/set-super-table.mutation';
|
||||
import { Disposable, toDisposable } from '@univerjs/core';
|
||||
import { RemoveSuperTableMutation, SetSuperTableMutation } from '../commands/mutations/set-super-table.mutation';
|
||||
import { IActiveDirtyManagerService } from '../services/active-dirty-manager.service';
|
||||
|
||||
export class SuperTableActiveDirtyController extends Disposable {
|
||||
constructor(@IActiveDirtyManagerService private readonly _activeDirtyManagerService: IActiveDirtyManagerService) {
|
||||
super();
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._activeDirtyManagerService.register(SetSuperTableMutation.id, {
|
||||
commandId: SetSuperTableMutation.id,
|
||||
getDirtyData: (command: ICommandInfo) => {
|
||||
const params = command.params as ISetSuperTableMutationParam;
|
||||
const tableNames = [params.tableName, params.oldTableName].filter((name): name is string => Boolean(name));
|
||||
return {
|
||||
dirtyRanges: [{ unitId: params.unitId, sheetId: params.reference.sheetId, range: params.reference.range }],
|
||||
dirtySuperTableMap: {
|
||||
[params.unitId]: Object.fromEntries(tableNames.map((name) => [name, '1'])),
|
||||
},
|
||||
clearDependencyTreeCache: {
|
||||
[params.unitId]: {
|
||||
[params.reference.sheetId]: '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
this._activeDirtyManagerService.register(RemoveSuperTableMutation.id, {
|
||||
commandId: RemoveSuperTableMutation.id,
|
||||
getDirtyData: (command: ICommandInfo) => {
|
||||
const params = command.params as IRemoveSuperTableMutationParam;
|
||||
return {
|
||||
dirtyRanges: params.reference
|
||||
? [{ unitId: params.unitId, sheetId: params.reference.sheetId, range: params.reference.range }]
|
||||
: undefined,
|
||||
dirtySuperTableMap: {
|
||||
[params.unitId]: {
|
||||
[params.tableName]: '1',
|
||||
},
|
||||
},
|
||||
clearDependencyTreeCache: params.reference
|
||||
? { [params.unitId]: { [params.reference.sheetId]: '1' } }
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
this.disposeWithMe(toDisposable(() => {
|
||||
this._activeDirtyManagerService.remove(SetSuperTableMutation.id);
|
||||
this._activeDirtyManagerService.remove(RemoveSuperTableMutation.id);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 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 { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { ISheetData } from '../../../basics/common';
|
||||
|
||||
import {
|
||||
CellValueType,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
ObjectMatrix,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel } from '../../../models/formula-data.model';
|
||||
import { CalculateFormulaService, ICalculateFormulaService } from '../../../services/calculate-formula.service';
|
||||
import { FormulaCurrentConfigService, IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
import { DefinedNamesService, IDefinedNamesService } from '../../../services/defined-names.service';
|
||||
import { DependencyManagerService, IDependencyManagerService } from '../../../services/dependency-manager.service';
|
||||
import {
|
||||
IFormulaExternalReferenceDataLoader,
|
||||
NoopFormulaExternalReferenceDataLoader,
|
||||
} from '../../../services/external-reference-data-loader.service';
|
||||
import {
|
||||
FeatureCalculationManagerService,
|
||||
IFeatureCalculationManagerService,
|
||||
} from '../../../services/feature-calculation-manager.service';
|
||||
import { FunctionService, IFunctionService } from '../../../services/function.service';
|
||||
import {
|
||||
HyperlinkEngineFormulaService,
|
||||
IHyperlinkEngineFormulaService,
|
||||
} from '../../../services/hyperlink-engine-formula.service';
|
||||
import {
|
||||
IOtherFormulaManagerService,
|
||||
OtherFormulaManagerService,
|
||||
} from '../../../services/other-formula-manager.service';
|
||||
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';
|
||||
import { LambdaParameterNodeFactory } from '../../ast-node/lambda-parameter-node';
|
||||
import { OperatorNodeFactory } from '../../ast-node/operator-node';
|
||||
import { PrefixNodeFactory } from '../../ast-node/prefix-node';
|
||||
import { ReferenceNodeFactory } from '../../ast-node/reference-node';
|
||||
import { SuffixNodeFactory } from '../../ast-node/suffix-node';
|
||||
import { UnionNodeFactory } from '../../ast-node/union-node';
|
||||
import { ValueNodeFactory } from '../../ast-node/value-node';
|
||||
import { FormulaDependencyGenerator, IFormulaDependencyGenerator } from '../../dependency/formula-dependency';
|
||||
import { Interpreter } from '../../interpreter/interpreter';
|
||||
import { Lexer } from '../lexer';
|
||||
import { LexerTreeBuilder } from '../lexer-tree-builder';
|
||||
import { AstTreeBuilder } from '../parser';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Main',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 4,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
2: {
|
||||
v: 'B2',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
3: {
|
||||
v: 'R2C2',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: ' ',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
2: {
|
||||
v: 1.23,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
3: {
|
||||
v: true,
|
||||
t: CellValueType.BOOLEAN,
|
||||
},
|
||||
4: {
|
||||
v: false,
|
||||
t: CellValueType.BOOLEAN,
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 0,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: '100',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
2: {
|
||||
v: '2.34',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
3: {
|
||||
v: 'test',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
4: {
|
||||
v: -3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Tool',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 4,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
2: {
|
||||
v: 'B2',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
3: {
|
||||
v: 'R2C2',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: ' ',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
2: {
|
||||
v: 1.23,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
3: {
|
||||
v: true,
|
||||
t: CellValueType.BOOLEAN,
|
||||
},
|
||||
4: {
|
||||
v: false,
|
||||
t: CellValueType.BOOLEAN,
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 0,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: '100',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
2: {
|
||||
v: '2.34',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
3: {
|
||||
v: 'test',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
4: {
|
||||
v: -3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
/**
|
||||
* This plugin hooks into Sheet's DI system to expose API to test scripts
|
||||
*/
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
private _formulaDataModel: FormulaDataModel | null = null;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
registerFormulaDependencies(this._injector);
|
||||
dependencies?.forEach((d) => this._injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData || TEST_WORKBOOK_DATA);
|
||||
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
const logService = get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // change this to `true` to debug tests via logs
|
||||
|
||||
const sheetData: ISheetData = {};
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const unitId = workbook.getUnitId();
|
||||
const sheetId = workbook.getActiveSheet()!.getSheetId();
|
||||
workbook.getSheets().forEach((sheet) => {
|
||||
const sheetConfig = sheet.getConfig();
|
||||
sheetData[sheet.getSheetId()] = {
|
||||
cellData: new ObjectMatrix(sheetConfig.cellData),
|
||||
rowCount: sheetConfig.rowCount,
|
||||
columnCount: sheetConfig.columnCount,
|
||||
rowData: sheetConfig.rowData,
|
||||
columnData: sheetConfig.columnData,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
unitId,
|
||||
sheetId,
|
||||
sheetData,
|
||||
};
|
||||
}
|
||||
|
||||
function registerFormulaDependencies(injector: Injector) {
|
||||
injector.add([ICalculateFormulaService, { useClass: CalculateFormulaService }]);
|
||||
injector.add([Lexer]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
|
||||
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
|
||||
injector.add([IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }]);
|
||||
injector.add([IFormulaExternalReferenceDataLoader, { useClass: NoopFormulaExternalReferenceDataLoader }]);
|
||||
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
|
||||
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([IOtherFormulaManagerService, { useClass: OtherFormulaManagerService }]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([ISuperTableService, { useClass: SuperTableService }]);
|
||||
injector.add([IFeatureCalculationManagerService, { useClass: FeatureCalculationManagerService }]);
|
||||
injector.add([IDependencyManagerService, { useClass: DependencyManagerService }]);
|
||||
injector.add([ISheetRowFilteredService, { useClass: SheetRowFilteredService }]);
|
||||
|
||||
injector.add([IFormulaDependencyGenerator, { useClass: FormulaDependencyGenerator }]);
|
||||
injector.add([Interpreter]);
|
||||
injector.add([AstTreeBuilder]);
|
||||
|
||||
injector.add([AstRootNodeFactory]);
|
||||
injector.add([FunctionNodeFactory]);
|
||||
injector.add([LambdaNodeFactory]);
|
||||
injector.add([LambdaParameterNodeFactory]);
|
||||
injector.add([OperatorNodeFactory]);
|
||||
injector.add([PrefixNodeFactory]);
|
||||
injector.add([ReferenceNodeFactory]);
|
||||
injector.add([SuffixNodeFactory]);
|
||||
injector.add([UnionNodeFactory]);
|
||||
injector.add([ValueNodeFactory]);
|
||||
injector.add([FormulaDataModel]);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* 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 { Injector } from '@univerjs/core';
|
||||
import type { FormulaDependencyTreeVirtual } from '../../dependency/dependency-tree';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ErrorType } from '../../../basics/error-type';
|
||||
import { IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
import { IOtherFormulaManagerService } from '../../../services/other-formula-manager.service';
|
||||
import { IFormulaRuntimeService } from '../../../services/runtime.service';
|
||||
import { FormulaDependencyTree } from '../../dependency/dependency-tree';
|
||||
import { IFormulaDependencyGenerator } from '../../dependency/formula-dependency';
|
||||
import { Interpreter } from '../../interpreter/interpreter';
|
||||
import { ErrorValueObject } from '../../value-object/base-value-object';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
describe('Test dependency', () => {
|
||||
let get: Injector['get'];
|
||||
let formulaDependencyGenerator: IFormulaDependencyGenerator;
|
||||
let formulaCurrentConfigService: IFormulaCurrentConfigService;
|
||||
let otherFormulaManagerService: IOtherFormulaManagerService;
|
||||
let testUnitId = 'test';
|
||||
let testSheetId = 'sheet1';
|
||||
let testSheetData = {};
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed();
|
||||
|
||||
get = testBed.get;
|
||||
|
||||
formulaDependencyGenerator = get(IFormulaDependencyGenerator);
|
||||
|
||||
formulaCurrentConfigService = get(IFormulaCurrentConfigService);
|
||||
|
||||
otherFormulaManagerService = get(IOtherFormulaManagerService);
|
||||
|
||||
const formulaRuntimeService = get(IFormulaRuntimeService);
|
||||
|
||||
const sheetItem = testBed.sheetData[testBed.sheetId];
|
||||
|
||||
testUnitId = testBed.unitId;
|
||||
testSheetId = testBed.sheetId;
|
||||
testSheetData = testBed.sheetData;
|
||||
|
||||
formulaRuntimeService.setCurrent(
|
||||
0,
|
||||
0,
|
||||
sheetItem.rowCount,
|
||||
sheetItem.columnCount,
|
||||
testBed.sheetId,
|
||||
testBed.unitId
|
||||
);
|
||||
});
|
||||
|
||||
describe('dependency normal', () => {
|
||||
it('test formula dependency simple ref', async () => {
|
||||
const testOtherFormulaId = 'sheet.cf_workbook-01_Q2oij1uNg7HLUT7aT2ikk_yhq9VWH_';
|
||||
const testOtherFormulaId2 = 'sheet.cf_workbook-01_Q2oij1uNg7HLUT7aT2ikk_idjs46G_';
|
||||
formulaCurrentConfigService.load({
|
||||
formulaData: {
|
||||
[testUnitId]: {
|
||||
[testSheetId]: {
|
||||
3: {
|
||||
12: {
|
||||
f: '=SUM(F27:J34)',
|
||||
},
|
||||
},
|
||||
9: {
|
||||
4: {
|
||||
f: '=A1:C4',
|
||||
},
|
||||
},
|
||||
16: {
|
||||
8: {
|
||||
f: '=SUM(E10:G13)',
|
||||
},
|
||||
},
|
||||
22: {
|
||||
6: {
|
||||
f: '=SUM(E10:I17)',
|
||||
},
|
||||
},
|
||||
26: {
|
||||
5: {
|
||||
f: '=E10:I23',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
unitId: testUnitId,
|
||||
sheetId: testSheetId,
|
||||
},
|
||||
],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {
|
||||
[testUnitId]: {
|
||||
[testSheetId]: {
|
||||
[testOtherFormulaId]: true,
|
||||
[testOtherFormulaId2]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
excludedCell: {},
|
||||
allUnitData: {
|
||||
[testUnitId]: testSheetData,
|
||||
},
|
||||
});
|
||||
|
||||
otherFormulaManagerService.batchRegister({
|
||||
[testUnitId]: {
|
||||
[testSheetId]: {
|
||||
[testOtherFormulaId]: {
|
||||
f: '=A1>1',
|
||||
ranges: [
|
||||
{
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 2,
|
||||
endColumn: 0,
|
||||
startAbsoluteRefType: 0,
|
||||
endAbsoluteRefType: 0,
|
||||
rangeType: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
[testOtherFormulaId2]: {
|
||||
f: '=A1>1',
|
||||
ranges: [
|
||||
{
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const treeList = await formulaDependencyGenerator.generate();
|
||||
const treeJson = treeList.map((tree) => tree instanceof FormulaDependencyTree ? tree.formula : tree.refTree?.formula).reverse();
|
||||
|
||||
expect(treeJson).toEqual(
|
||||
[
|
||||
'=A1:C4',
|
||||
'=SUM(E10:G13)',
|
||||
'=SUM(E10:I17)',
|
||||
'=E10:I23',
|
||||
'=SUM(F27:J34)',
|
||||
'=A1>1',
|
||||
'=A1>1',
|
||||
'=A1>1',
|
||||
'=A1>1',
|
||||
]
|
||||
);
|
||||
|
||||
// Check the offset position of other formula
|
||||
// Note that the order is reversed
|
||||
const tree = treeList[2] as FormulaDependencyTreeVirtual;
|
||||
expect(tree.refOffsetX).toEqual(0);
|
||||
expect(tree.refOffsetY).toEqual(1);
|
||||
|
||||
const tree2 = treeList[1] as FormulaDependencyTreeVirtual;
|
||||
expect(tree2.refOffsetX).toEqual(0);
|
||||
expect(tree2.refOffsetY).toEqual(2);
|
||||
});
|
||||
|
||||
it('limits other formulas without sheet dimensions to one result slot', async () => {
|
||||
const formulaId = 'formula.shape-unit_shape_shape_shape-1_test';
|
||||
formulaCurrentConfigService.load({
|
||||
formulaData: {},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {
|
||||
'shape-unit': {
|
||||
shape: {
|
||||
[formulaId]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
excludedCell: {},
|
||||
allUnitData: {
|
||||
[testUnitId]: testSheetData,
|
||||
},
|
||||
});
|
||||
|
||||
otherFormulaManagerService.batchRegister({
|
||||
'shape-unit': {
|
||||
shape: {
|
||||
[formulaId]: {
|
||||
f: '=1',
|
||||
ranges: [{
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 2,
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const treeList = await formulaDependencyGenerator.generate();
|
||||
const shapeTrees = treeList.filter((tree) => tree.formulaId === formulaId);
|
||||
|
||||
expect(shapeTrees).toHaveLength(1);
|
||||
expect(shapeTrees[0]).toBeInstanceOf(FormulaDependencyTree);
|
||||
});
|
||||
|
||||
it('should generate virtual dependency trees for shared formulas with offsets', async () => {
|
||||
formulaCurrentConfigService.load({
|
||||
formulaData: {
|
||||
[testUnitId]: {
|
||||
[testSheetId]: {
|
||||
0: {
|
||||
1: {
|
||||
f: '=A1',
|
||||
si: 'shared-formula-1',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
1: {
|
||||
f: '=A1',
|
||||
si: 'shared-formula-1',
|
||||
x: 0,
|
||||
y: 1,
|
||||
},
|
||||
},
|
||||
2: {
|
||||
1: {
|
||||
f: '=A1',
|
||||
si: 'shared-formula-1',
|
||||
x: 0,
|
||||
y: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: true,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: {
|
||||
[testUnitId]: testSheetData,
|
||||
},
|
||||
});
|
||||
|
||||
const treeList = await formulaDependencyGenerator.generate();
|
||||
const realTree = treeList.find((tree): tree is FormulaDependencyTree => tree instanceof FormulaDependencyTree && tree.row === 0 && tree.column === 1);
|
||||
const virtualTrees = treeList.filter((tree): tree is FormulaDependencyTreeVirtual => !(tree instanceof FormulaDependencyTree));
|
||||
|
||||
expect(realTree?.formula).toBe('=A1');
|
||||
expect(virtualTrees).toHaveLength(2);
|
||||
expect(virtualTrees
|
||||
.map((tree) => [tree.refTree, tree.refOffsetX, tree.refOffsetY])
|
||||
.sort((a, b) => Number(a[2]) - Number(b[2]))).toEqual([
|
||||
[realTree, 0, 1],
|
||||
[realTree, 0, 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores a non-reference result while collecting dependency ranges', async () => {
|
||||
formulaCurrentConfigService.load({
|
||||
formulaData: {
|
||||
[testUnitId]: {
|
||||
[testSheetId]: {
|
||||
0: {
|
||||
0: { f: '=A1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: true,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: {
|
||||
[testUnitId]: testSheetData,
|
||||
},
|
||||
});
|
||||
vi.spyOn(get(Interpreter), 'execute').mockReturnValue(ErrorValueObject.create(ErrorType.REF));
|
||||
|
||||
await expect(formulaDependencyGenerator.generate()).resolves.toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* 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 { Injector, IWorkbookData, Univer, Workbook } from '@univerjs/core';
|
||||
import type { LexerNode } from '../lexer-node';
|
||||
import { LocaleType } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { FUNCTION_NAMES_DATE } from '../../../functions/date/function-names';
|
||||
import { Month } from '../../../functions/date/month';
|
||||
import { IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
import { IDefinedNamesService } from '../../../services/defined-names.service';
|
||||
import { IFunctionService } from '../../../services/function.service';
|
||||
import { Lexer } from '../lexer';
|
||||
import { LexerTreeBuilder } from '../lexer-tree-builder';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheetName1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 4,
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 44,
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 444,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
describe('lexer test', () => {
|
||||
let univer: Univer;
|
||||
let lexer: Lexer;
|
||||
let get: Injector['get'];
|
||||
let workbook: Workbook;
|
||||
let definedNamesService: IDefinedNamesService;
|
||||
let formulaCurrentConfigService: IFormulaCurrentConfigService;
|
||||
let functionService: IFunctionService;
|
||||
let lexerTreeBuilder: LexerTreeBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA);
|
||||
univer = testBed.univer;
|
||||
workbook = testBed.sheet;
|
||||
get = testBed.get;
|
||||
|
||||
definedNamesService = get(IDefinedNamesService);
|
||||
formulaCurrentConfigService = get(IFormulaCurrentConfigService);
|
||||
functionService = get(IFunctionService);
|
||||
lexerTreeBuilder = get(LexerTreeBuilder);
|
||||
|
||||
formulaCurrentConfigService.setExecuteUnitId('test');
|
||||
formulaCurrentConfigService.setExecuteSubUnitId('sheet1');
|
||||
formulaCurrentConfigService.setSheetNameMap({ test: { sheet1: 'sheetName1' } });
|
||||
|
||||
// runtimeService.setCurrent(0, 0, 4, 1, 'sheet1', 'test');
|
||||
|
||||
lexer = new Lexer(
|
||||
definedNamesService,
|
||||
lexerTreeBuilder,
|
||||
formulaCurrentConfigService,
|
||||
functionService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('lexer definedName', () => {
|
||||
it('simple', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test1',
|
||||
name: 'myName',
|
||||
formulaOrRefString: '$A$10:$C$100',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder('=myName') as LexerNode;
|
||||
|
||||
expect(node.serialize()).toStrictEqual({
|
||||
token: 'R_1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [
|
||||
{
|
||||
token: ':',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [
|
||||
{
|
||||
token: 'P_1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [
|
||||
{
|
||||
token: '$A$10',
|
||||
st: 0,
|
||||
ed: 4,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
token: 'P_1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [
|
||||
{
|
||||
token: '$C$100',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('moves sheet-local relative references from the current formula cell and wraps grid edges', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'previous-row',
|
||||
name: 'PrevRow',
|
||||
formulaOrRefString: 'sheetName1!$A1048576',
|
||||
localSheetId: 'sheet1',
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'same-row',
|
||||
name: 'SameRow',
|
||||
formulaOrRefString: 'sheetName1!$C1',
|
||||
localSheetId: 'sheet1',
|
||||
});
|
||||
|
||||
const previousRow = lexer.treeBuilder('=PrevRow', true, 'test', 0, 8) as LexerNode;
|
||||
const nextPreviousRow = lexer.treeBuilder('=PrevRow', true, 'test', 0, 9) as LexerNode;
|
||||
const sameRow = lexer.treeBuilder('=SameRow', true, 'test', 5, 73) as LexerNode;
|
||||
|
||||
expect(JSON.stringify(previousRow.serialize())).toContain('sheetName1!$A8');
|
||||
expect(JSON.stringify(nextPreviousRow.serialize())).toContain('sheetName1!$A9');
|
||||
expect(JSON.stringify(sameRow.serialize())).toContain('sheetName1!$C74');
|
||||
});
|
||||
|
||||
it('resolves workbook defined names that point to sheets with underscores', () => {
|
||||
formulaCurrentConfigService.setSheetNameMap({
|
||||
test: {
|
||||
sheet1: 'sheetName1',
|
||||
sheet2: 'Parameters_Constants',
|
||||
},
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test-underscore-sheet',
|
||||
name: 'CONST_GJpt',
|
||||
formulaOrRefString: 'Parameters_Constants!$B$28',
|
||||
localSheetId: 'AllDefaultWorkbook',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder('=CONST_GJpt') as LexerNode;
|
||||
|
||||
expect(JSON.stringify(node.serialize())).toContain('Parameters_Constants!$B$28');
|
||||
expect(JSON.stringify(node.serialize())).not.toContain('#NAME?');
|
||||
});
|
||||
|
||||
it('resolves defined names case-insensitively', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test-case-insensitive',
|
||||
name: 'CONST_kNm3',
|
||||
formulaOrRefString: '$B$27',
|
||||
localSheetId: 'AllDefaultWorkbook',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder('=CONST_KNM3') as LexerNode;
|
||||
|
||||
expect(JSON.stringify(node.serialize())).toContain('$B$27');
|
||||
expect(JSON.stringify(node.serialize())).not.toContain('#NAME?');
|
||||
});
|
||||
|
||||
it('prefers sheet-local defined names over workbook defined names', () => {
|
||||
formulaCurrentConfigService.setExecuteSubUnitId('sheet-weekly');
|
||||
formulaCurrentConfigService.setSheetNameMap({
|
||||
test: {
|
||||
'sheet-weekly': 'Weekly',
|
||||
'sheet-report': 'Report',
|
||||
},
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'date-begin-local',
|
||||
name: 'date_begin',
|
||||
formulaOrRefString: 'Weekly!$F$4',
|
||||
localSheetId: 'sheet-weekly',
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'date-begin-global',
|
||||
name: 'date_begin',
|
||||
formulaOrRefString: 'Report!$F$4',
|
||||
localSheetId: 'AllDefaultWorkbook',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder('=date_begin+6') as LexerNode;
|
||||
const serialized = JSON.stringify(node.serialize());
|
||||
|
||||
expect(serialized).toContain('Weekly!$F$4');
|
||||
expect(serialized).not.toContain('Report!$F$4');
|
||||
});
|
||||
|
||||
it('isolates cached defined names by sheet', () => {
|
||||
formulaCurrentConfigService.setSheetNameMap({
|
||||
test: {
|
||||
'sheet-report': 'Report',
|
||||
'sheet-weekly': 'Weekly',
|
||||
},
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'date-begin-report',
|
||||
name: 'date_begin',
|
||||
formulaOrRefString: 'Report!$F$4',
|
||||
localSheetId: 'sheet-report',
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'date-begin-weekly',
|
||||
name: 'date_begin',
|
||||
formulaOrRefString: 'Weekly!$F$4',
|
||||
localSheetId: 'sheet-weekly',
|
||||
});
|
||||
|
||||
formulaCurrentConfigService.setExecuteSubUnitId('sheet-report');
|
||||
const reportNode = lexer.treeBuilder('=date_begin+1') as LexerNode;
|
||||
formulaCurrentConfigService.setExecuteSubUnitId('sheet-weekly');
|
||||
const weeklyNode = lexer.treeBuilder('=date_begin+1') as LexerNode;
|
||||
|
||||
expect(JSON.stringify(reportNode.serialize())).toContain('Report!$F$4');
|
||||
expect(JSON.stringify(weeklyNode.serialize())).toContain('Weekly!$F$4');
|
||||
});
|
||||
|
||||
it('resolves defined names next to operators and spaces', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test-operator-left',
|
||||
name: 'CONST_GJ',
|
||||
formulaOrRefString: '$B$25',
|
||||
localSheetId: 'AllDefaultWorkbook',
|
||||
});
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test-operator-right',
|
||||
name: 'CONST_t',
|
||||
formulaOrRefString: '$B$26',
|
||||
localSheetId: 'AllDefaultWorkbook',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder('=CONST_GJ & "/" &CONST_t') as LexerNode;
|
||||
const serialized = JSON.stringify(node.serialize());
|
||||
|
||||
expect(serialized).toContain('$B$25');
|
||||
expect(serialized).toContain('$B$26');
|
||||
expect(serialized).not.toContain('#NAME?');
|
||||
});
|
||||
|
||||
it('lambda', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test2',
|
||||
name: 'myName',
|
||||
formulaOrRefString: 'lambda(x, y , x*x*y)',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder(
|
||||
'=myName(1+sum(A1:B1), 100)'
|
||||
) as LexerNode;
|
||||
expect(node.serialize()).toStrictEqual({
|
||||
token: 'R_1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [
|
||||
{
|
||||
token: 'lambda',
|
||||
st: 0,
|
||||
ed: 5,
|
||||
children: [
|
||||
{
|
||||
token: 'L_1',
|
||||
st: 16,
|
||||
ed: 18,
|
||||
children: [
|
||||
{
|
||||
token: 'P_1',
|
||||
st: 17,
|
||||
ed: 19,
|
||||
children: [
|
||||
'1',
|
||||
{
|
||||
token: 'sum',
|
||||
st: 23,
|
||||
ed: 25,
|
||||
children: [
|
||||
{
|
||||
token: 'P_1',
|
||||
st: 23,
|
||||
ed: 25,
|
||||
children: [
|
||||
{
|
||||
token: ':',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children: [
|
||||
{
|
||||
token: 'P_1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children:
|
||||
[
|
||||
{
|
||||
token: 'A1',
|
||||
st: 27,
|
||||
ed: 28,
|
||||
children:
|
||||
[],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
token: 'P_1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children:
|
||||
[
|
||||
{
|
||||
token: 'B1',
|
||||
st: -1,
|
||||
ed: -1,
|
||||
children:
|
||||
[],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'+',
|
||||
],
|
||||
},
|
||||
{
|
||||
token: 'P_1',
|
||||
st: 30,
|
||||
ed: 32,
|
||||
children: ['100'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ token: 'P_1', st: 3, ed: 5, children: ['x'] },
|
||||
{ token: 'P_1', st: 5, ed: 7, children: ['y '] },
|
||||
{
|
||||
token: 'P_1',
|
||||
st: 9,
|
||||
ed: 11,
|
||||
children: ['x', 'x', '*', 'y', '*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers a registered function over a same-name defined name', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'month-defined-name',
|
||||
name: 'MONTH',
|
||||
formulaOrRefString: '$B$5',
|
||||
});
|
||||
functionService.registerExecutors(new Month(FUNCTION_NAMES_DATE.MONTH));
|
||||
|
||||
const node = lexer.treeBuilder('=MONTH(D10)') as LexerNode;
|
||||
const serialized = JSON.stringify(node.serialize());
|
||||
|
||||
expect(serialized).toContain('MONTH');
|
||||
expect(serialized).toContain('D10');
|
||||
expect(serialized).not.toContain('$B$5');
|
||||
});
|
||||
|
||||
it('resolves a same-name defined name when it is not a function call', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'month-defined-name',
|
||||
name: 'month',
|
||||
formulaOrRefString: '$B$5',
|
||||
});
|
||||
functionService.registerExecutors(new Month(FUNCTION_NAMES_DATE.MONTH));
|
||||
|
||||
const node = lexer.treeBuilder('=month+1') as LexerNode;
|
||||
const serialized = JSON.stringify(node.serialize());
|
||||
|
||||
expect(serialized).toContain('$B$5');
|
||||
expect(serialized).not.toContain('#NAME?');
|
||||
});
|
||||
|
||||
it('defined name nest struct', () => {
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test1',
|
||||
name: 'myName1',
|
||||
localSheetId: 'sheet1',
|
||||
formulaOrRefString: '$A$10:$C$100',
|
||||
});
|
||||
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test2',
|
||||
name: 'myName2',
|
||||
formulaOrRefString: '=sheetName1!myName1 + 100',
|
||||
});
|
||||
|
||||
definedNamesService.registerDefinedName('test', {
|
||||
id: 'test3',
|
||||
name: 'myName3',
|
||||
formulaOrRefString: '=myName2',
|
||||
});
|
||||
|
||||
const node = lexer.treeBuilder('=myName3') as LexerNode;
|
||||
|
||||
expect(node.serialize()).toStrictEqual({
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
ed: 4,
|
||||
st: 0,
|
||||
token: '$A$10',
|
||||
},
|
||||
],
|
||||
ed: -1,
|
||||
st: -1,
|
||||
token: 'P_1',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
ed: -1,
|
||||
st: -1,
|
||||
token: '$C$100',
|
||||
},
|
||||
],
|
||||
ed: -1,
|
||||
st: -1,
|
||||
token: 'P_1',
|
||||
},
|
||||
],
|
||||
ed: -1,
|
||||
st: -1,
|
||||
token: ':',
|
||||
},
|
||||
'100',
|
||||
'+',
|
||||
],
|
||||
ed: -1,
|
||||
st: -1,
|
||||
token: 'R_1',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,938 @@
|
||||
/**
|
||||
* 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 { Injector } from '@univerjs/core';
|
||||
import type { BaseAstNode } from '../../ast-node/base-ast-node';
|
||||
import type { BaseReferenceObject } from '../../reference-object/base-reference-object';
|
||||
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_INFORMATION } from '../../../functions/information/function-names';
|
||||
import { Isblank } from '../../../functions/information/isblank';
|
||||
import { FUNCTION_NAMES_LOGICAL } from '../../../functions/logical/function-names';
|
||||
import { Groupby } from '../../../functions/logical/groupby';
|
||||
import { If } from '../../../functions/logical/if';
|
||||
import { Or } from '../../../functions/logical/or';
|
||||
import { Percentof } from '../../../functions/logical/percentof';
|
||||
import { FUNCTION_NAMES_LOOKUP } from '../../../functions/lookup/function-names';
|
||||
import { Hstack } from '../../../functions/lookup/hstack';
|
||||
import { Index } from '../../../functions/lookup/index';
|
||||
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';
|
||||
import { Sum } from '../../../functions/math/sum';
|
||||
import { Compare } from '../../../functions/meta/compare';
|
||||
import { Divided } from '../../../functions/meta/divided';
|
||||
import { FUNCTION_NAMES_META } from '../../../functions/meta/function-names';
|
||||
import { Minus } from '../../../functions/meta/minus';
|
||||
import { Multiply } from '../../../functions/meta/multiply';
|
||||
import { Plus } from '../../../functions/meta/plus';
|
||||
import { Counta } from '../../../functions/statistical/counta';
|
||||
import { Countif } from '../../../functions/statistical/countif';
|
||||
import { FUNCTION_NAMES_STATISTICAL } from '../../../functions/statistical/function-names';
|
||||
import { StdevP } from '../../../functions/statistical/stdev-p';
|
||||
import { FUNCTION_NAMES_TEXT } from '../../../functions/text/function-names';
|
||||
import { Regexmatch } from '../../../functions/text/regexmatch';
|
||||
import { IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
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';
|
||||
import { AstTreeBuilder } from '../parser';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
describe('Test indirect', () => {
|
||||
let get: Injector['get'];
|
||||
let lexer: Lexer;
|
||||
let astTreeBuilder: AstTreeBuilder;
|
||||
let interpreter: Interpreter;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed();
|
||||
|
||||
get = testBed.get;
|
||||
|
||||
lexer = get(Lexer);
|
||||
astTreeBuilder = get(AstTreeBuilder);
|
||||
interpreter = get(Interpreter);
|
||||
|
||||
const functionService = get(IFunctionService);
|
||||
|
||||
const formulaCurrentConfigService = get(IFormulaCurrentConfigService);
|
||||
|
||||
const formulaRuntimeService = get(IFormulaRuntimeService);
|
||||
|
||||
const superTableService = get(ISuperTableService);
|
||||
|
||||
formulaCurrentConfigService.load({
|
||||
formulaData: {},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
});
|
||||
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];
|
||||
|
||||
formulaRuntimeService.setCurrent(
|
||||
0,
|
||||
0,
|
||||
sheetItem.rowCount,
|
||||
sheetItem.columnCount,
|
||||
testBed.sheetId,
|
||||
testBed.unitId
|
||||
);
|
||||
|
||||
functionService.registerExecutors(
|
||||
new Sum(FUNCTION_NAMES_MATH.SUM),
|
||||
new Subtotal(FUNCTION_NAMES_MATH.SUBTOTAL),
|
||||
new Compare(FUNCTION_NAMES_META.COMPARE),
|
||||
new Plus(FUNCTION_NAMES_META.PLUS),
|
||||
new Minus(FUNCTION_NAMES_META.MINUS),
|
||||
new Multiply(FUNCTION_NAMES_META.MULTIPLY),
|
||||
new Divided(FUNCTION_NAMES_META.DIVIDED),
|
||||
new Pi(FUNCTION_NAMES_MATH.PI),
|
||||
new Countif(FUNCTION_NAMES_STATISTICAL.COUNTIF),
|
||||
new Counta(FUNCTION_NAMES_STATISTICAL.COUNTA),
|
||||
new StdevP(FUNCTION_NAMES_STATISTICAL.STDEV_P),
|
||||
new Regexmatch(FUNCTION_NAMES_TEXT.REGEXMATCH),
|
||||
new Hstack(FUNCTION_NAMES_LOOKUP.HSTACK),
|
||||
new Index(FUNCTION_NAMES_LOOKUP.INDEX),
|
||||
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 Or(FUNCTION_NAMES_LOGICAL.OR),
|
||||
new Percentof(FUNCTION_NAMES_LOGICAL.PERCENTOF),
|
||||
new Isblank(FUNCTION_NAMES_INFORMATION.ISBLANK)
|
||||
);
|
||||
|
||||
superTableService.registerTable(testBed.unitId, 'Table1', {
|
||||
sheetId: testBed.sheetId,
|
||||
titleMap: new Map([
|
||||
['col1', 0],
|
||||
['col2', 1],
|
||||
['col3', 2],
|
||||
['col4', 3],
|
||||
['CASH\nOUT', 0],
|
||||
['CASH\nIN', 1],
|
||||
['Blank\nColumn', 4],
|
||||
['Deposit,\nCredit (+)', 0],
|
||||
['Business Name:', 3],
|
||||
['F\n(1-5)', 4],
|
||||
['S\n(1-5)', 4],
|
||||
['D\n(1-10)', 4],
|
||||
]),
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 3,
|
||||
startColumn: 0,
|
||||
endColumn: 4,
|
||||
},
|
||||
});
|
||||
superTableService.registerTable(testBed.unitId, 'Table_FMEA', {
|
||||
sheetId: testBed.sheetId,
|
||||
titleMap: new Map([
|
||||
['F\n(1-5)', 4],
|
||||
['S\n(1-5)', 4],
|
||||
['D\n(1-10)', 4],
|
||||
]),
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 3,
|
||||
startColumn: 0,
|
||||
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 astNode = astTreeBuilder.parse(lexerNode as LexerNode) as BaseAstNode;
|
||||
const groupbyNode = astNode.getChildren()[0];
|
||||
const hstackNode = groupbyNode.getChildren()[2];
|
||||
|
||||
expect(groupbyNode.getToken()).toBe('GROUPBY');
|
||||
expect(hstackNode.getToken()).toBe('HSTACK');
|
||||
expect(hstackNode.getChildren().map((node) => node.getToken())).toEqual(['COUNTA', 'PERCENTOF']);
|
||||
});
|
||||
|
||||
it('Ref error', async () => {
|
||||
const lexerNode = lexer.treeBuilder(`=sum(${ErrorType.REF} + 1)`);
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
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('keeps external Table source data when INDEX returns a selected cell reference', () => {
|
||||
const lexerNode = lexer.treeBuilder('=INDEX(Sales.xlsx!SalesTable[Amount],1)');
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
const reference = result as BaseReferenceObject;
|
||||
expect({
|
||||
unitId: reference.getUnitId(),
|
||||
sheetId: reference.getSheetId(),
|
||||
forcedUnitId: reference.getForcedUnitId(),
|
||||
forcedSheetId: reference.getForcedSheetId(),
|
||||
forcedSheetName: reference.getForcedSheetName(),
|
||||
}).toEqual({
|
||||
unitId: 'sales-source',
|
||||
sheetId: 'source-sheet',
|
||||
forcedUnitId: 'sales-source',
|
||||
forcedSheetId: 'source-sheet',
|
||||
forcedSheetName: '',
|
||||
});
|
||||
expect(reference.getCellByPosition().getValue()).toBe(10);
|
||||
});
|
||||
|
||||
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))`);
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(ErrorType.NAME);
|
||||
});
|
||||
|
||||
it('Minus Minus one', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=--1');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it('Minus Minus Minus one', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=---1');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(-1);
|
||||
});
|
||||
|
||||
it('Plus Plus Plus Plus Plus Plus one', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=++++++1');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it('Plus Plus Plus ref', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=+++++++A1');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as ArrayValueObject).getFirstCell().getValue()).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it('normalizes xlfn REGEXTEST to REGEXMATCH during formula execution', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=_xlfn.REGEXTEST(D4,"^(?=.*e)test$")');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('normalizes xlfn STDEV.P to registered statistical function during formula execution', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=_xlfn.STDEV.P(0,0,0)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it('treats a numeric zero cell as non-blank in IF comparisons', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=IF(A4="","blank","not blank")');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual('not blank');
|
||||
});
|
||||
|
||||
it('parses xlfn LAMBDA parameters with xlpm prefixes', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=_xlfn.LAMBDA(_xlpm.a,_xlpm.i,_xlpm.a)(1,2)');
|
||||
|
||||
expect(() => astTreeBuilder.parse(lexerNode as LexerNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Minus Minus Minus ref', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=---A1');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(-1);
|
||||
});
|
||||
|
||||
it('Minus Minus Minus Minus sum', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=----sum(A1:A2)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('Cross tab sum', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=----sum(Tool!A1:A2)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('Reference column', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=sum(A:A)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(5);
|
||||
});
|
||||
|
||||
it('Not exist formula', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=notExistFormula(A:A)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(ErrorType.NAME);
|
||||
});
|
||||
|
||||
it('Reference row', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(1:1)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(3);
|
||||
});
|
||||
|
||||
it('Error #NAME?', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=A1:A');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(ErrorType.NAME);
|
||||
});
|
||||
|
||||
it('Root node has multiple parameter!', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=PI()/1.570796327,M54');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('error as return #NUM!', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(#NUM! + #VALUE!) + #SPILL! - (#CALC!)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(ErrorType.NUM);
|
||||
});
|
||||
|
||||
it('LET formula as parameter nest', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=LET(x,2,y,x+3,x+y+3)');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(10);
|
||||
});
|
||||
|
||||
it('test missing formula input handles gracefully', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=1/3+');
|
||||
|
||||
expect(lexerNode).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('test incomplete formula input', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=+');
|
||||
|
||||
expect(lexerNode).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('test formula input validation with function', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=sum(A1+)');
|
||||
|
||||
expect(lexerNode).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('test formula input suffix', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=%');
|
||||
|
||||
expect(lexerNode).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('test formula input suffix correctly', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=10%');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(0.1);
|
||||
});
|
||||
|
||||
it('test formula function has operator parameter', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=sum(-)');
|
||||
|
||||
expect(lexerNode).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('test formula no parameter', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=sum(,,-)');
|
||||
|
||||
expect(lexerNode).toStrictEqual(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('test array string error', async () => {
|
||||
const lexerNode = lexer.treeBuilder('="{1,2,3;4,5,6}"');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual('{1,2,3;4,5,6}');
|
||||
});
|
||||
|
||||
it('test array formula correctly', async () => {
|
||||
const lexerNode = lexer.treeBuilder('={10,2,3;4,5,6}');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as ArrayValueObject).getFirstCell().getValue()).toStrictEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Structured references — Table1 (Main)', () => {
|
||||
it('test super formula correctly', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[col1]:[col4]])');
|
||||
|
||||
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
|
||||
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(9.23);
|
||||
});
|
||||
|
||||
it('sums a single column: Table1[col1] => 3 + 1 + 0 = 4', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[col1])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('sums a 2-column data range: Table1[[#Data],[col1]:[col2]] => (3+1+0) + (4) = 8', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[#Data],[col1]:[col2]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(8);
|
||||
});
|
||||
|
||||
it('sums headers only: Table1[[#Headers],[col1]:[col4]] => 1 + 2 = 3 (text ignored)', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[#Headers],[col1]:[col4]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(3);
|
||||
});
|
||||
|
||||
it('sums a single column across #All: Table1[[#All],[col3]] => 1.23', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[#All],[col3]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(1.23);
|
||||
});
|
||||
|
||||
it('counts numbers in data column: SUM(Table1[[#Data],[col2]]) => 1', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[#Data],[col2]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('sums a multi-column range: Table1[[col1]:[col4]] => 9.23 (baseline)', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[col1]:[col4]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(9.23);
|
||||
});
|
||||
|
||||
it('sums all data across the table: SUM(Table1[#Data]) => 9.23', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[#Data])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(6.23);
|
||||
});
|
||||
|
||||
it('totals row behavior: SUM(Table1[[#Totals],[col1]]) => 0 when no totals row is configured', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[#Totals],[col1]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it('unions two columns: SUM(Table1[col1], Table1[col3]) => (3+1+0) + (1.23) = 5.23', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[col1], Table1[col3])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(5.23);
|
||||
});
|
||||
|
||||
it('mixed qualifier order is supported: Table1[[col1]:[col2],[#Data]] => 8', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[col1]:[col2],[#Data]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual('#NAME?');
|
||||
});
|
||||
|
||||
it('single-column double-bracket form: Table1[[#Data],[col1]] => 3+1+0 = 4', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[[#Data],[col1]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('supports line-break column names in structured references', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[CASH\r\nOUT])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('supports commas and line breaks in structured-reference column names', () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[Deposit,\r\nCredit (+)])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('supports trailing colons in structured-reference column names', () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUBTOTAL(103,Table1[Business Name:])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(3);
|
||||
});
|
||||
|
||||
it('resolves blank current-row cells with normalized line-break column names', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=ISBLANK(Table1[[#This Row],[Blank\r\nColumn]])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toBe(true);
|
||||
});
|
||||
|
||||
it('calculates FMEA current-row formulas with parenthesized column names', () => {
|
||||
const lexerNode = lexer.treeBuilder(
|
||||
'=IF(OR(ISBLANK(Table_FMEA[[#This Row],[F\r\n(1-5)]]),ISBLANK(Table_FMEA[[#This Row],[S\r\n(1-5)]]))," - ",Table_FMEA[[#This Row],[F\r\n(1-5)]]*Table_FMEA[[#This Row],[S\r\n(1-5)]])'
|
||||
) as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
|
||||
expect((result as BaseValueObject).getValue()).toBe(' - ');
|
||||
});
|
||||
|
||||
it('supports line-break column names in SUBTOTAL structured references', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUBTOTAL(109,Table1[CASH\r\nOUT])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
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 astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(101);
|
||||
});
|
||||
|
||||
it('ignores text in numeric aggregations: SUM(Table1[col2]) => 4', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=SUM(Table1[col2])') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(4);
|
||||
});
|
||||
|
||||
it('COUNTIF on a column with mixed types: COUNTIF(Table1[col2], ">0") => 1', async () => {
|
||||
const lexerNode = lexer.treeBuilder('=COUNTIF(Table1[col2], ">0")') as LexerNode;
|
||||
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
expect((result as BaseValueObject).getValue()).toStrictEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
204
packages/engine-formula/src/engine/analysis/lexer-node.ts
Normal file
204
packages/engine-formula/src/engine/analysis/lexer-node.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
|
||||
import type { LambdaPrivacyVarType } from '../ast-node/base-ast-node';
|
||||
import { DEFAULT_TOKEN_TYPE_ROOT } from '../../basics/token-type';
|
||||
|
||||
interface ILexerNodeJson {
|
||||
token: string;
|
||||
children: Array<ILexerNodeJson | string>;
|
||||
}
|
||||
|
||||
export class LexerNode {
|
||||
private _parent: Nullable<LexerNode>;
|
||||
|
||||
private _token: string = DEFAULT_TOKEN_TYPE_ROOT;
|
||||
|
||||
private _children: Array<LexerNode | string> = [];
|
||||
|
||||
private _lambdaId: Nullable<string>;
|
||||
|
||||
private _functionDefinitionPrivacyVar: Nullable<LambdaPrivacyVarType>;
|
||||
|
||||
private _lambdaParameter: string = '';
|
||||
|
||||
private _startIndex: number = -1;
|
||||
|
||||
private _endIndex: number = -1;
|
||||
|
||||
private _definedNames: Array<string> = [];
|
||||
|
||||
dispose() {
|
||||
this._children.forEach((node) => {
|
||||
if (!(typeof node === 'string')) {
|
||||
node.dispose();
|
||||
}
|
||||
});
|
||||
this._functionDefinitionPrivacyVar?.clear();
|
||||
|
||||
this._functionDefinitionPrivacyVar = null;
|
||||
|
||||
this._children = [];
|
||||
|
||||
this._parent = null;
|
||||
|
||||
this._definedNames = [];
|
||||
}
|
||||
|
||||
getDefinedNames() {
|
||||
return this._definedNames;
|
||||
}
|
||||
|
||||
getStartIndex() {
|
||||
return this._startIndex;
|
||||
}
|
||||
|
||||
getLambdaId() {
|
||||
return this._lambdaId;
|
||||
}
|
||||
|
||||
setLambdaId(lambdaId: string) {
|
||||
this._lambdaId = lambdaId;
|
||||
}
|
||||
|
||||
getFunctionDefinitionPrivacyVar() {
|
||||
return this._functionDefinitionPrivacyVar;
|
||||
}
|
||||
|
||||
setLambdaPrivacyVar(lambdaPrivacyVar: LambdaPrivacyVarType) {
|
||||
this._functionDefinitionPrivacyVar = lambdaPrivacyVar;
|
||||
}
|
||||
|
||||
getLambdaParameter() {
|
||||
return this._lambdaParameter;
|
||||
}
|
||||
|
||||
setLambdaParameter(lambdaParameter: string) {
|
||||
this._lambdaParameter = lambdaParameter;
|
||||
}
|
||||
|
||||
getParent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
setParent(lexerNode: LexerNode) {
|
||||
this._parent = lexerNode;
|
||||
}
|
||||
|
||||
getChildren() {
|
||||
return this._children;
|
||||
}
|
||||
|
||||
setChildren(children: Array<LexerNode | string>) {
|
||||
this._children = children;
|
||||
}
|
||||
|
||||
addChildren(children: LexerNode | string) {
|
||||
this._children.push(children);
|
||||
}
|
||||
|
||||
addChildrenFirst(children: LexerNode | string) {
|
||||
this._children.unshift(children);
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this._token;
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this._token = token;
|
||||
}
|
||||
|
||||
setIndex(st: number, ed: number) {
|
||||
this._startIndex = st;
|
||||
this._endIndex = ed;
|
||||
}
|
||||
|
||||
setDefinedNames(definedNames: Array<string>) {
|
||||
this._definedNames = definedNames;
|
||||
}
|
||||
|
||||
hasDefinedNames() {
|
||||
return this._definedNames.length > 0;
|
||||
}
|
||||
|
||||
replaceChild(lexerNode: LexerNode, newLexerNode: LexerNode) {
|
||||
const i = this._getIndexInParent(lexerNode);
|
||||
|
||||
if (i == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getChildren().splice(i, 1, newLexerNode);
|
||||
|
||||
newLexerNode.setParent(this);
|
||||
}
|
||||
|
||||
changeToParent(newParentLexerNode: LexerNode) {
|
||||
const parentNode = this.getParent();
|
||||
if (parentNode) {
|
||||
parentNode.removeChild(this);
|
||||
}
|
||||
this.setParent(newParentLexerNode);
|
||||
const childrenNode = newParentLexerNode.getChildren();
|
||||
childrenNode.push(this);
|
||||
}
|
||||
|
||||
removeChild(lexerNode: LexerNode) {
|
||||
const i = this._getIndexInParent(lexerNode);
|
||||
|
||||
if (i == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getChildren().splice(i, 1);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const token = this.getToken();
|
||||
const children = this.getChildren();
|
||||
|
||||
const childrenSerialization: Array<ILexerNodeJson | string> = [];
|
||||
const childrenCount = children.length;
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
if (item instanceof LexerNode) {
|
||||
childrenSerialization.push(item.serialize());
|
||||
} else {
|
||||
childrenSerialization.push(item);
|
||||
}
|
||||
}
|
||||
return {
|
||||
token,
|
||||
st: this._startIndex,
|
||||
ed: this._endIndex,
|
||||
children: childrenSerialization,
|
||||
};
|
||||
}
|
||||
|
||||
private _getIndexInParent(lexerNode: LexerNode) {
|
||||
const childrenNode = this.getChildren();
|
||||
const childrenCount = childrenNode.length;
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const child = childrenNode[i];
|
||||
if (child === lexerNode) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2543
packages/engine-formula/src/engine/analysis/lexer-tree-builder.ts
Normal file
2543
packages/engine-formula/src/engine/analysis/lexer-tree-builder.ts
Normal file
File diff suppressed because it is too large
Load Diff
50
packages/engine-formula/src/engine/analysis/lexer.ts
Normal file
50
packages/engine-formula/src/engine/analysis/lexer.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
|
||||
import { IDefinedNamesService } from '../../services/defined-names.service';
|
||||
import { IFunctionService } from '../../services/function.service';
|
||||
import { LexerTreeBuilder } from './lexer-tree-builder';
|
||||
|
||||
export class Lexer extends Disposable {
|
||||
constructor(
|
||||
@IDefinedNamesService private readonly _definedNamesService: IDefinedNamesService,
|
||||
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder,
|
||||
@IFormulaCurrentConfigService private readonly _formulaCurrentConfigService: IFormulaCurrentConfigService,
|
||||
@IFunctionService private readonly _functionService: IFunctionService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
treeBuilder(formulaString: string, transformSuffix = true, unitId?: Nullable<string>, refOffsetX = 0, refOffsetY = 0) {
|
||||
if (this._definedNamesService.getAllDefinedNamesIsEmpty()) {
|
||||
return this._lexerTreeBuilder.treeBuilder(formulaString, transformSuffix);
|
||||
}
|
||||
|
||||
return this._lexerTreeBuilder.treeBuilder(formulaString, transformSuffix, {
|
||||
unitId: unitId ?? this._formulaCurrentConfigService.getExecuteUnitId(),
|
||||
sheetId: this._formulaCurrentConfigService.getExecuteSubUnitId(),
|
||||
refOffsetX,
|
||||
refOffsetY,
|
||||
hasFunction: this._functionService.hasExecutor.bind(this._functionService),
|
||||
getValueByName: this._definedNamesService.getValueByName.bind(this._definedNamesService),
|
||||
getDirtyDefinedNameMap: this._formulaCurrentConfigService.getDirtyDefinedNameMap.bind(this._formulaCurrentConfigService),
|
||||
getSheetName: this._formulaCurrentConfigService.getSheetName.bind(this._formulaCurrentConfigService),
|
||||
});
|
||||
}
|
||||
}
|
||||
437
packages/engine-formula/src/engine/analysis/parser.ts
Normal file
437
packages/engine-formula/src/engine/analysis/parser.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { LambdaNode } from '../ast-node/lambda-node';
|
||||
import { Disposable, Inject, sortRules } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import {
|
||||
DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME,
|
||||
DEFAULT_TOKEN_LET_FUNCTION_NAME,
|
||||
DEFAULT_TOKEN_TYPE_LAMBDA_PARAMETER,
|
||||
DEFAULT_TOKEN_TYPE_PARAMETER,
|
||||
DEFAULT_TOKEN_TYPE_ROOT,
|
||||
FORCED_RECALCULATION_FUNCTION_NAME,
|
||||
} from '../../basics/token-type';
|
||||
import { IFormulaRuntimeService } from '../../services/runtime.service';
|
||||
import { AstRootNode, AstRootNodeFactory } from '../ast-node/ast-root-node';
|
||||
import { BaseAstNode, ErrorNode } from '../ast-node/base-ast-node';
|
||||
import { ErrorFunctionNode, FunctionNodeFactory } from '../ast-node/function-node';
|
||||
import { LambdaNodeFactory } from '../ast-node/lambda-node';
|
||||
import { LambdaParameterNodeFactory } from '../ast-node/lambda-parameter-node';
|
||||
import { NodeType } from '../ast-node/node-type';
|
||||
import { NullNode } from '../ast-node/null-node';
|
||||
import { OperatorNodeFactory } from '../ast-node/operator-node';
|
||||
import { PrefixNodeFactory } from '../ast-node/prefix-node';
|
||||
import { ReferenceNodeFactory } from '../ast-node/reference-node';
|
||||
import { SuffixNodeFactory } from '../ast-node/suffix-node';
|
||||
import { UnionNodeFactory } from '../ast-node/union-node';
|
||||
import { ValueNodeFactory } from '../ast-node/value-node';
|
||||
import { getAstNodeTopParent } from '../utils/ast-node-tool';
|
||||
import { isChildRunTimeParameter, isFirstChildParameter } from '../utils/function-definition';
|
||||
import { updateLambdaStatement } from '../utils/update-lambda-statement';
|
||||
import { LexerNode } from './lexer-node';
|
||||
|
||||
export class AstTreeBuilder extends Disposable {
|
||||
private _astNodeFactoryList: AstRootNodeFactory[] = [];
|
||||
|
||||
constructor(
|
||||
@IFormulaRuntimeService private readonly _runtimeService: IFormulaRuntimeService,
|
||||
@Inject(AstRootNodeFactory) private readonly _astRootNodeFactory: AstRootNodeFactory,
|
||||
@Inject(FunctionNodeFactory) private readonly _functionNodeFactory: FunctionNodeFactory,
|
||||
@Inject(LambdaNodeFactory) private readonly _lambdaNodeFactory: LambdaNodeFactory,
|
||||
@Inject(LambdaParameterNodeFactory) private readonly _lambdaParameterNodeFactory: LambdaParameterNodeFactory,
|
||||
@Inject(OperatorNodeFactory) private readonly _operatorNodeFactory: OperatorNodeFactory,
|
||||
@Inject(PrefixNodeFactory) private readonly _prefixNodeFactory: PrefixNodeFactory,
|
||||
@Inject(ReferenceNodeFactory) private readonly _referenceNodeFactory: ReferenceNodeFactory,
|
||||
@Inject(SuffixNodeFactory) private readonly _suffixNodeFactory: SuffixNodeFactory,
|
||||
@Inject(UnionNodeFactory) private readonly _unionNodeFactory: UnionNodeFactory,
|
||||
@Inject(ValueNodeFactory) private readonly _valueNodeFactory: ValueNodeFactory
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initializeAstNode();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._astNodeFactoryList.forEach((nodeFactory) => {
|
||||
nodeFactory.dispose();
|
||||
});
|
||||
|
||||
this._astNodeFactoryList = [];
|
||||
}
|
||||
|
||||
parse(lexerNode: LexerNode) {
|
||||
const astNode = new AstRootNode(DEFAULT_TOKEN_TYPE_ROOT);
|
||||
const node = this._parse(lexerNode, astNode);
|
||||
|
||||
/**
|
||||
* If the lexer node has defined names,
|
||||
* it means that the current formula contains a reference to the defined name.
|
||||
*/
|
||||
if (lexerNode.hasDefinedNames()) {
|
||||
node?.setDefinedNames(lexerNode.getDefinedNames());
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private _lambdaParameterHandler(lexerNode: LexerNode, parent: LambdaNode) {
|
||||
if (parent.getLambdaId == null) {
|
||||
return ErrorNode.create(ErrorType.VALUE);
|
||||
}
|
||||
const lambdaId = parent.getLambdaId();
|
||||
|
||||
const parentAstNode = new AstRootNode(DEFAULT_TOKEN_TYPE_ROOT);
|
||||
|
||||
// const lambdaRuntime = this._parserDataLoader.getLambdaRuntime();
|
||||
const currentLambdaPrivacyVar = this._runtimeService.getFunctionDefinitionPrivacyVar(lambdaId);
|
||||
|
||||
if (!currentLambdaPrivacyVar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentLambdaPrivacyVarKeys = [...currentLambdaPrivacyVar.keys()];
|
||||
|
||||
const children = lexerNode.getChildren();
|
||||
const childrenCount = children.length;
|
||||
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
|
||||
if (item instanceof LexerNode) {
|
||||
updateLambdaStatement(item, lambdaId, currentLambdaPrivacyVar);
|
||||
|
||||
this._parse(item, parentAstNode);
|
||||
|
||||
const parentChildren = parentAstNode.getChildren();
|
||||
const valueNode = parentChildren[i];
|
||||
if (valueNode != null) {
|
||||
currentLambdaPrivacyVar.set(currentLambdaPrivacyVarKeys[i], valueNode);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// astNode?.setParent(parentAstNode);
|
||||
}
|
||||
|
||||
// const parentChildren = parentAstNode.getChildren();
|
||||
// const parentChildrenCount = parentChildren.length;
|
||||
|
||||
// for (let i = 0; i < parentChildrenCount; i++) {
|
||||
// const item = parentChildren[i];
|
||||
// currentLambdaPrivacyVar.set(currentLambdaPrivacyVarKeys[i], item);
|
||||
// }
|
||||
|
||||
parentAstNode.setParent(parent);
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
private _changeLetToLambda(letLexerNode: LexerNode) {
|
||||
const letChildren = letLexerNode.getChildren();
|
||||
|
||||
const letChildrenCount = letChildren.length;
|
||||
if (letChildrenCount % 2 !== 1 || letChildrenCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newLambdaNode = new LexerNode();
|
||||
newLambdaNode.setToken(DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME);
|
||||
|
||||
const newLambdaParameterNode = new LexerNode();
|
||||
newLambdaParameterNode.setToken(DEFAULT_TOKEN_TYPE_LAMBDA_PARAMETER);
|
||||
|
||||
// const lastLetChild = letChildren[letChildrenCount - 1];
|
||||
|
||||
const copyChildren = [...letChildren];
|
||||
|
||||
for (let i = 0; i < letChildrenCount; i++) {
|
||||
const child = copyChildren[i];
|
||||
if (!(child instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (i % 2 === 0) {
|
||||
child.changeToParent(newLambdaNode);
|
||||
} else {
|
||||
child.changeToParent(newLambdaParameterNode);
|
||||
}
|
||||
}
|
||||
|
||||
newLambdaNode.addChildrenFirst(newLambdaParameterNode);
|
||||
|
||||
newLambdaParameterNode.setParent(newLambdaNode);
|
||||
|
||||
const parent = letLexerNode.getParent();
|
||||
|
||||
parent?.replaceChild(letLexerNode, newLambdaNode);
|
||||
|
||||
return newLambdaNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
private _parse(lexerNode: LexerNode, parent: BaseAstNode): Nullable<BaseAstNode> {
|
||||
const children = lexerNode.getChildren();
|
||||
const childrenCount = children.length;
|
||||
const calculateStack: BaseAstNode[] = [];
|
||||
let currentAstNode: Nullable<BaseAstNode> = null;
|
||||
// console.log('lexerNode', lexerNode, children);
|
||||
const token = lexerNode.getToken().trim().toUpperCase();
|
||||
if (token === DEFAULT_TOKEN_LET_FUNCTION_NAME) {
|
||||
/**
|
||||
* Since the let function performs the same as lambda expressions,
|
||||
* here we make a conversion to use only lambda expressions for calculations.
|
||||
*/
|
||||
const resultNode: Nullable<LexerNode> = this._changeLetToLambda(lexerNode);
|
||||
if (resultNode != null) {
|
||||
return this._parse(resultNode, parent);
|
||||
}
|
||||
|
||||
// console.log('errorLet', resultNode, currentAstNode, lexerNode);
|
||||
return ErrorNode.create(ErrorType.ERROR);
|
||||
}
|
||||
if (token === DEFAULT_TOKEN_TYPE_PARAMETER) {
|
||||
currentAstNode = parent;
|
||||
|
||||
if (childrenCount === 0) {
|
||||
const nullNode = new NullNode(DEFAULT_TOKEN_TYPE_ROOT);
|
||||
|
||||
nullNode.setParent(parent);
|
||||
|
||||
return currentAstNode;
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Process the parameters of the lambda expression, create an execution environment,
|
||||
* allowing parameters to correspond to variables.
|
||||
*/
|
||||
if (token === DEFAULT_TOKEN_TYPE_LAMBDA_PARAMETER) {
|
||||
let resultNode: BaseAstNode | false = this._lambdaParameterHandler(lexerNode, parent as LambdaNode);
|
||||
if (resultNode === false) {
|
||||
// console.log('error1', resultNode, currentAstNode, lexerNode);
|
||||
resultNode = ErrorNode.create(ErrorType.ERROR);
|
||||
}
|
||||
|
||||
return resultNode;
|
||||
}
|
||||
|
||||
currentAstNode = this._checkAstNode(lexerNode);
|
||||
if (currentAstNode == null) {
|
||||
// console.log('error2', currentAstNode, lexerNode);
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
|
||||
// currentAstNode.setParent(parent);
|
||||
// parent.addChildren(currentAstNode);
|
||||
}
|
||||
// console.log('currentAstNode', currentAstNode.nodeType, currentAstNode, lexerNode);
|
||||
const firstChild = children[0];
|
||||
// let isSkipFirstInLambda = false;
|
||||
// if (
|
||||
// currentAstNode.getToken().trim().toUpperCase() === DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME &&
|
||||
// !isFirstChildParameter(firstChild)
|
||||
// ) {
|
||||
// isSkipFirstInLambda = true;
|
||||
// }
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
/**
|
||||
* If it is a lambda expression, by default, only the first and last child nodes are valid.
|
||||
* The intermediate nodes have already been processed during the conversion of lambda parameters,
|
||||
* so they need to be skipped here.
|
||||
*/
|
||||
if (isFirstChildParameter(firstChild)) {
|
||||
if (i !== 0 && i !== childrenCount - 1) {
|
||||
continue;
|
||||
}
|
||||
} else if (isChildRunTimeParameter(item)) {
|
||||
if (i !== childrenCount - 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let astNode: Nullable<BaseAstNode> = null;
|
||||
if (item instanceof LexerNode) {
|
||||
if (item.getToken() === DEFAULT_TOKEN_TYPE_PARAMETER && item.getChildren().length === 0) {
|
||||
// =trim( ) is #NAME?
|
||||
const children = item.getParent()?.getChildren();
|
||||
if (children && children.length === 1) {
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
}
|
||||
|
||||
astNode = this._parse(item, currentAstNode);
|
||||
if (astNode === currentAstNode) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
astNode = this._checkAstNode(item);
|
||||
}
|
||||
|
||||
if (astNode == null) {
|
||||
// console.log('error3', astNode, currentAstNode, lexerNode);
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
|
||||
astNode = getAstNodeTopParent(astNode);
|
||||
|
||||
if (astNode == null || astNode?.nodeType === NodeType.ERROR) {
|
||||
return astNode;
|
||||
}
|
||||
|
||||
// console.log('bugfix1', astNode, astNode.nodeType, currentAstNode, lexerNode);
|
||||
switch (astNode.nodeType) {
|
||||
// case NodeType.ERROR:
|
||||
// return astNode;
|
||||
case NodeType.BASE:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.FUNCTION: {
|
||||
const token = astNode.getToken().trim().toUpperCase();
|
||||
if (FORCED_RECALCULATION_FUNCTION_NAME.has(token)) {
|
||||
astNode.setForcedCalculateFunction();
|
||||
}
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
}
|
||||
case NodeType.LAMBDA:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.LAMBDA_PARAMETER:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.OPERATOR: {
|
||||
const parameterNode1 = calculateStack.pop();
|
||||
const parameterNode2 = calculateStack.pop();
|
||||
|
||||
if (parameterNode2) {
|
||||
parameterNode2.setParent(astNode);
|
||||
}
|
||||
|
||||
if (parameterNode1) {
|
||||
parameterNode1.setParent(astNode);
|
||||
}
|
||||
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
}
|
||||
case NodeType.REFERENCE:
|
||||
// astNode.setRefOffset(this._refOffsetX, this._refOffsetY);
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.ROOT:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.UNION:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.VALUE:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.PREFIX:
|
||||
// this._setPrefixRefOffset(astNode);
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
case NodeType.SUFFIX:
|
||||
calculateStack.push(astNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const calculateStackCount = calculateStack.length;
|
||||
|
||||
for (let i = 0; i < calculateStackCount; i++) {
|
||||
const item = calculateStack[i];
|
||||
item.setParent(currentAstNode);
|
||||
}
|
||||
|
||||
return currentAstNode;
|
||||
}
|
||||
|
||||
// private _setPrefixRefOffset(astNode: BaseAstNode) {
|
||||
// const children = astNode.getChildren();
|
||||
// const childrenCount = children.length;
|
||||
// for (let i = 0; i < childrenCount; i++) {
|
||||
// const item = children[i];
|
||||
// if (item.nodeType === NodeType.REFERENCE) {
|
||||
// item.setRefOffset(this._refOffsetX, this._refOffsetY);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private _checkAstNode(item: LexerNode | string) {
|
||||
let astNode: Nullable<BaseAstNode> = null;
|
||||
const astNodeFactoryListCount = this._astNodeFactoryList.length;
|
||||
for (let x = 0; x < astNodeFactoryListCount; x++) {
|
||||
const astNodeFactory = this._astNodeFactoryList[x];
|
||||
astNode = astNodeFactory.checkAndCreateNodeType(item);
|
||||
if (astNode != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (astNode == null) {
|
||||
const aggregatorNode = this._createGroupByAggregatorTokenNode(item);
|
||||
if (aggregatorNode != null) {
|
||||
return aggregatorNode;
|
||||
}
|
||||
|
||||
return new ErrorFunctionNode(typeof item === 'string' ? item : item.getToken());
|
||||
}
|
||||
|
||||
// console.log('astNode111', astNode, item);
|
||||
return astNode;
|
||||
}
|
||||
|
||||
private _createGroupByAggregatorTokenNode(param: LexerNode | string): Nullable<BaseAstNode> {
|
||||
const token = (typeof param === 'string' ? param : param.getToken()).trim().toUpperCase();
|
||||
const prefix = '_XLETA.';
|
||||
if (!token.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = token.slice(prefix.length);
|
||||
if (
|
||||
name !== 'SUM' &&
|
||||
name !== 'COUNT' &&
|
||||
name !== 'COUNTA' &&
|
||||
name !== 'PERCENTOF' &&
|
||||
name !== 'MIN' &&
|
||||
name !== 'MAX' &&
|
||||
name !== 'ARRAYTOTEXT'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BaseAstNode(name);
|
||||
}
|
||||
|
||||
private _initializeAstNode() {
|
||||
this._astNodeFactoryList = [
|
||||
this._astRootNodeFactory,
|
||||
this._functionNodeFactory,
|
||||
this._lambdaNodeFactory,
|
||||
this._lambdaParameterNodeFactory,
|
||||
this._operatorNodeFactory,
|
||||
this._prefixNodeFactory,
|
||||
this._referenceNodeFactory,
|
||||
this._suffixNodeFactory,
|
||||
this._unionNodeFactory,
|
||||
this._valueNodeFactory,
|
||||
].sort(sortRules);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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 { IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { LambdaValueObjectObject } from '../../value-object/lambda-value-object';
|
||||
import { DateSystem, LocaleType, UniverInstanceType } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ErrorType } from '../../../basics/error-type';
|
||||
import { createFunctionTestBed } from '../../../functions/__tests__/create-function-test-bed';
|
||||
import { AsyncCustomFunction, CustomFunction } from '../../../functions/custom-function';
|
||||
import { DateFunction } from '../../../functions/date/date';
|
||||
import { FUNCTION_NAMES_DATE } from '../../../functions/date/function-names';
|
||||
import { Accrint } from '../../../functions/financial/accrint';
|
||||
import { FUNCTION_NAMES_FINANCIAL } from '../../../functions/financial/function-names';
|
||||
import { Abs } from '../../../functions/math/abs';
|
||||
import { FUNCTION_NAMES_MATH } from '../../../functions/math/function-names';
|
||||
import { FUNCTION_NAMES_META } from '../../../functions/meta/function-names';
|
||||
import { Plus } from '../../../functions/meta/plus';
|
||||
import { FUNCTION_NAMES_TEXT } from '../../../functions/text/function-names';
|
||||
import { Substitute } from '../../../functions/text/substitute';
|
||||
import { getObjectValue } from '../../../functions/util';
|
||||
import { IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
import { IFunctionService } from '../../../services/function.service';
|
||||
import { IFormulaRuntimeService } from '../../../services/runtime.service';
|
||||
import { Lexer } from '../../analysis/lexer';
|
||||
import { LexerNode } from '../../analysis/lexer-node';
|
||||
import { AstTreeBuilder } from '../../analysis/parser';
|
||||
import { Interpreter } from '../../interpreter/interpreter';
|
||||
import { generateExecuteAstNodeData } from '../../utils/ast-node-tool';
|
||||
import { BaseAstNode } from '../base-ast-node';
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.FR_FR,
|
||||
name: 'Locale-independent date parsing',
|
||||
sheetOrder: ['sheet1'],
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 1,
|
||||
columnCount: 1,
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createDateSystemWorkbookData(id: string, dateSystem: DateSystem): IWorkbookData {
|
||||
return {
|
||||
...createWorkbookData(),
|
||||
id,
|
||||
locale: LocaleType.EN_US,
|
||||
name: id,
|
||||
dateSystem,
|
||||
};
|
||||
}
|
||||
|
||||
describe('FunctionNode workbook context', () => {
|
||||
it('does not use the executing workbook locale to parse formula text', () => {
|
||||
const testBed = createFunctionTestBed(createWorkbookData());
|
||||
const currentConfigService = testBed.get(IFormulaCurrentConfigService);
|
||||
const runtimeService = testBed.get(IFormulaRuntimeService);
|
||||
const lexer = testBed.get(Lexer);
|
||||
const astTreeBuilder = testBed.get(AstTreeBuilder);
|
||||
const interpreter = testBed.get(Interpreter);
|
||||
|
||||
currentConfigService.load({
|
||||
formulaData: {},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: { [testBed.unitId]: testBed.sheetData },
|
||||
});
|
||||
runtimeService.setCurrent(0, 0, 1, 1, testBed.sheetId, testBed.unitId);
|
||||
testBed.get(IFunctionService).registerExecutors(new Accrint(FUNCTION_NAMES_FINANCIAL.ACCRINT));
|
||||
testBed.get(IFunctionService).registerExecutors(new Plus(FUNCTION_NAMES_META.PLUS));
|
||||
|
||||
const calculate = (formula: string) => {
|
||||
const lexerNode = lexer.treeBuilder(formula);
|
||||
if (!(lexerNode instanceof LexerNode)) throw new TypeError(`Failed to parse formula: ${formula}`);
|
||||
const astNode = astTreeBuilder.parse(lexerNode);
|
||||
if (!(astNode instanceof BaseAstNode)) throw new TypeError(`Failed to build AST: ${formula}`);
|
||||
return getObjectValue(interpreter.execute(generateExecuteAstNodeData(astNode)));
|
||||
};
|
||||
|
||||
expect(calculate('=ACCRINT("31/01/2024","31/07/2024","01/05/2024",0.1,1000,2)'))
|
||||
.toBe(ErrorType.VALUE);
|
||||
expect(calculate('=1+"1,5"')).toBe(ErrorType.VALUE);
|
||||
});
|
||||
|
||||
it('uses the executing workbook date system for implicit string coercion', async () => {
|
||||
const testBed = createFunctionTestBed(createDateSystemWorkbookData('date-1900', DateSystem.Date1900));
|
||||
const date1904Workbook = testBed.univer.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
createDateSystemWorkbookData('date-1904', DateSystem.Date1904)
|
||||
);
|
||||
const currentConfigService = testBed.get(IFormulaCurrentConfigService);
|
||||
const runtimeService = testBed.get(IFormulaRuntimeService);
|
||||
const lexer = testBed.get(Lexer);
|
||||
const astTreeBuilder = testBed.get(AstTreeBuilder);
|
||||
const interpreter = testBed.get(Interpreter);
|
||||
|
||||
currentConfigService.load({
|
||||
formulaData: {},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: { [testBed.unitId]: testBed.sheetData },
|
||||
});
|
||||
testBed.get(IFunctionService).registerExecutors(new Plus(FUNCTION_NAMES_META.PLUS));
|
||||
testBed.get(IFunctionService).registerExecutors(new Abs(FUNCTION_NAMES_MATH.ABS));
|
||||
testBed.get(IFunctionService).registerExecutors(new Substitute(FUNCTION_NAMES_TEXT.SUBSTITUTE));
|
||||
testBed.get(IFunctionService).registerExecutors(new DateFunction(FUNCTION_NAMES_DATE.DATE));
|
||||
const customDate = new CustomFunction('CUSTOMDATE');
|
||||
customDate.calculateCustom = () => '1904-1-1';
|
||||
const customDateArray = new CustomFunction('CUSTOMDATEARRAY');
|
||||
customDateArray.calculateCustom = () => [['1904-1-1']];
|
||||
const asyncCustomDate = new AsyncCustomFunction('ASYNCCUSTOMDATE');
|
||||
const date1904Result = createDeferred<string>();
|
||||
const date1900Result = createDeferred<string>();
|
||||
const date1904Started = createDeferred<void>();
|
||||
const date1900Started = createDeferred<void>();
|
||||
asyncCustomDate.calculateCustom = (dateSystem) => {
|
||||
if (dateSystem === '1904') {
|
||||
date1904Started.resolve();
|
||||
return date1904Result.promise;
|
||||
}
|
||||
|
||||
date1900Started.resolve();
|
||||
return date1900Result.promise;
|
||||
};
|
||||
testBed.get(IFunctionService).registerExecutors(customDate, customDateArray, asyncCustomDate);
|
||||
|
||||
const calculate = (unitId: string, sheetId: string, formula = '="1904-1-1"+0') => {
|
||||
runtimeService.setCurrent(0, 0, 1, 1, sheetId, unitId);
|
||||
const lexerNode = lexer.treeBuilder(formula);
|
||||
if (!(lexerNode instanceof LexerNode)) throw new TypeError('Failed to parse implicit date coercion formula');
|
||||
const astNode = astTreeBuilder.parse(lexerNode);
|
||||
if (!(astNode instanceof BaseAstNode)) throw new TypeError('Failed to build implicit date coercion AST');
|
||||
return getObjectValue(interpreter.execute(generateExecuteAstNodeData(astNode)));
|
||||
};
|
||||
const calculateAsync = async (unitId: string, sheetId: string, formula: string) => {
|
||||
runtimeService.setCurrent(0, 0, 1, 1, sheetId, unitId);
|
||||
const lexerNode = lexer.treeBuilder(formula);
|
||||
if (!(lexerNode instanceof LexerNode)) throw new TypeError('Failed to parse async custom function formula');
|
||||
const astNode = astTreeBuilder.parse(lexerNode);
|
||||
if (!(astNode instanceof BaseAstNode)) throw new TypeError('Failed to build async custom function AST');
|
||||
return getObjectValue(await interpreter.executeAsync(generateExecuteAstNodeData(astNode)));
|
||||
};
|
||||
|
||||
expect(calculate(testBed.unitId, testBed.sheetId)).toBe(1462);
|
||||
expect(calculate(date1904Workbook.getUnitId(), date1904Workbook.getActiveSheet()!.getSheetId())).toBe(0);
|
||||
expect(calculate(testBed.unitId, testBed.sheetId, '=DATE(1904,1,1)')).toBe(1462);
|
||||
expect(calculate(
|
||||
date1904Workbook.getUnitId(),
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
'=DATE(1904,1,1)'
|
||||
)).toBe(0);
|
||||
expect(calculate(
|
||||
date1904Workbook.getUnitId(),
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
'=ABS("1904-1-1")'
|
||||
)).toBe(0);
|
||||
expect(calculate(
|
||||
date1904Workbook.getUnitId(),
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
'=ABS(SUBSTITUTE("1904/1/1","/","-"))'
|
||||
)).toBe(0);
|
||||
expect(calculate(testBed.unitId, testBed.sheetId, '=ABS(SUBSTITUTE("1904/1/1","/","-"))')).toBe(1462);
|
||||
expect(calculate(
|
||||
date1904Workbook.getUnitId(),
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
'=ABS(CUSTOMDATE())'
|
||||
)).toBe(0);
|
||||
expect(calculate(
|
||||
date1904Workbook.getUnitId(),
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
'=ABS(CUSTOMDATEARRAY())'
|
||||
)).toStrictEqual([[0]]);
|
||||
|
||||
runtimeService.setCurrent(
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
date1904Workbook.getUnitId()
|
||||
);
|
||||
const lambdaLexerNode = lexer.treeBuilder('=LAMBDA(value,ABS(value))');
|
||||
if (!(lambdaLexerNode instanceof LexerNode)) throw new TypeError('Failed to parse lambda formula');
|
||||
const lambdaAstNode = astTreeBuilder.parse(lambdaLexerNode);
|
||||
if (!(lambdaAstNode instanceof BaseAstNode)) throw new TypeError('Failed to build lambda AST');
|
||||
const lambda = interpreter.execute(generateExecuteAstNodeData(lambdaAstNode)) as LambdaValueObjectObject;
|
||||
expect(lambda.executeCustom('1904-1-1').getValue()).toBe(0);
|
||||
|
||||
const pending1904 = calculateAsync(
|
||||
date1904Workbook.getUnitId(),
|
||||
date1904Workbook.getActiveSheet()!.getSheetId(),
|
||||
'=ABS(ASYNCCUSTOMDATE("1904"))'
|
||||
);
|
||||
await date1904Started.promise;
|
||||
const pending1900 = calculateAsync(testBed.unitId, testBed.sheetId, '=ABS(ASYNCCUSTOMDATE("1900"))');
|
||||
await date1900Started.promise;
|
||||
date1904Result.resolve('1904-1-1');
|
||||
expect(await pending1904).toBe(0);
|
||||
date1900Result.resolve('1904-1-1');
|
||||
expect(await pending1900).toBe(1462);
|
||||
|
||||
expect(calculate(testBed.unitId, testBed.sheetId)).toBe(1462);
|
||||
});
|
||||
});
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((promiseResolve) => {
|
||||
resolve = promiseResolve;
|
||||
});
|
||||
|
||||
return { promise, resolve };
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 { IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
import type { IFormulaUnitReferenceResolver } from '../../../services/unit-reference-resolver.service';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ReferenceObjectType } from '../../utils/value-object';
|
||||
import { BaseAstNode } from '../base-ast-node';
|
||||
import { ReferenceNode } from '../reference-node';
|
||||
import { UnionNode } from '../union-node';
|
||||
|
||||
describe('ReferenceNode external range loading', () => {
|
||||
it('loads the full colon range from the qualified left reference', async () => {
|
||||
const load = vi.fn(async () => undefined);
|
||||
const currentConfig = {
|
||||
getSheetNameMap: () => ({}),
|
||||
getUnitData: () => ({}),
|
||||
getArrayFormulaCellData: () => ({}),
|
||||
getArrayFormulaRange: () => ({}),
|
||||
getUnitStylesData: () => ({}),
|
||||
};
|
||||
const runtime = {
|
||||
currentUnitId: 'host',
|
||||
currentSubUnitId: 'host-sheet',
|
||||
currentRow: 0,
|
||||
currentColumn: 0,
|
||||
getUnitData: () => ({}),
|
||||
getRuntimeArrayFormulaCellData: () => ({}),
|
||||
getUnitArrayFormula: () => ({}),
|
||||
getRuntimeFeatureCellData: () => ({}),
|
||||
};
|
||||
const resolver = {
|
||||
resolve: () => ({
|
||||
unitId: 'source',
|
||||
externalReference: {
|
||||
kind: 'host',
|
||||
qualifier: 'Sales',
|
||||
referenceId: 'sales',
|
||||
},
|
||||
}),
|
||||
} satisfies IFormulaUnitReferenceResolver;
|
||||
const superTableService = {
|
||||
getTableMap: () => null,
|
||||
getTableOptionMap: () => new Map(),
|
||||
};
|
||||
const left = new ReferenceNode(
|
||||
currentConfig,
|
||||
runtime,
|
||||
"'[Sales]Data'!A1",
|
||||
ReferenceObjectType.CELL,
|
||||
resolver,
|
||||
superTableService,
|
||||
{ load },
|
||||
true
|
||||
);
|
||||
const right = new BaseAstNode('B2');
|
||||
const union = new UnionNode(':', {
|
||||
getSheetsInfo: () => ({
|
||||
sheetNameMap: {},
|
||||
sheetOrder: [],
|
||||
}),
|
||||
} satisfies Pick<IFormulaCurrentConfigService, 'getSheetsInfo'>);
|
||||
left.setParent(union);
|
||||
right.setParent(union);
|
||||
|
||||
await left.executeAsync();
|
||||
|
||||
expect(load).toHaveBeenCalledWith(expect.objectContaining({
|
||||
token: "'[Sales]Data'!A1:B2",
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IUnitRange, IWorkbookData } from '@univerjs/core';
|
||||
import type { ISheetData } from '../../../basics/common';
|
||||
import type { IFormulaDependencyTree } from '../../dependency/dependency-tree';
|
||||
|
||||
import { CellValueType, LocaleType } from '@univerjs/core';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFunctionTestBed } from '../../../functions/__tests__/create-function-test-bed';
|
||||
import { FUNCTION_NAMES_LOGICAL } from '../../../functions/logical/function-names';
|
||||
import { Let } from '../../../functions/logical/let';
|
||||
import { Choosecols } from '../../../functions/lookup/choosecols';
|
||||
import { Filter } from '../../../functions/lookup/filter';
|
||||
import { FUNCTION_NAMES_LOOKUP } from '../../../functions/lookup/function-names';
|
||||
import { Vstack } from '../../../functions/lookup/vstack';
|
||||
import { FUNCTION_NAMES_MATH } from '../../../functions/math/function-names';
|
||||
import { Sum } from '../../../functions/math/sum';
|
||||
import { Compare } from '../../../functions/meta/compare';
|
||||
import { FUNCTION_NAMES_META } from '../../../functions/meta/function-names';
|
||||
import { getObjectValue } from '../../../functions/util';
|
||||
import { IFormulaCurrentConfigService } from '../../../services/current-data.service';
|
||||
import { DependencyManagerService, IDependencyManagerService } from '../../../services/dependency-manager.service';
|
||||
import {
|
||||
FeatureCalculationManagerService,
|
||||
IFeatureCalculationManagerService,
|
||||
} from '../../../services/feature-calculation-manager.service';
|
||||
import { IFunctionService } from '../../../services/function.service';
|
||||
import { IFormulaRuntimeService } from '../../../services/runtime.service';
|
||||
import { Lexer } from '../../analysis/lexer';
|
||||
import { LexerNode } from '../../analysis/lexer-node';
|
||||
import { AstTreeBuilder } from '../../analysis/parser';
|
||||
import { FormulaDependencyTree } from '../../dependency/dependency-tree';
|
||||
import { IFormulaDependencyGenerator } from '../../dependency/formula-dependency';
|
||||
import { Interpreter } from '../../interpreter/interpreter';
|
||||
import { generateExecuteAstNodeData } from '../../utils/ast-node-tool';
|
||||
import { BaseAstNode } from '../base-ast-node';
|
||||
|
||||
class CapturingDependencyManagerService extends DependencyManagerService {
|
||||
readonly capturedRanges = new Map<number, IUnitRange[]>();
|
||||
|
||||
override addDependencyRTreeCache(tree: IFormulaDependencyTree): void {
|
||||
this.capturedRanges.set(tree.treeId, [...tree.rangeList]);
|
||||
super.addDependencyRTreeCache(tree);
|
||||
}
|
||||
}
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
const sampleRowsBySheet: Record<string, Array<Array<number | string>>> = {
|
||||
jan: [
|
||||
[45148, 'Red', 1, 9],
|
||||
[45166, 'Blue', 7, 70],
|
||||
[45163, 'Green', 2, 16],
|
||||
],
|
||||
feb: [
|
||||
[45174, 'Purple', 6, 72],
|
||||
[45177, 'Red', 2, 18],
|
||||
[45184, 'Purple', 2, 24],
|
||||
[45188, 'Green', 6, 48],
|
||||
[45194, 'Purple', 1, 12],
|
||||
],
|
||||
mar: [
|
||||
[45200, 'Blue', 5, 50],
|
||||
[45205, 'Red', 4, 28],
|
||||
[45210, 'Green', 4, 32],
|
||||
[45213, 'Purple', 3, 36],
|
||||
],
|
||||
};
|
||||
sampleRowsBySheet.sheet1 = sampleRowsBySheet.jan;
|
||||
sampleRowsBySheet.sheet2 = sampleRowsBySheet.feb;
|
||||
sampleRowsBySheet.sheet3 = sampleRowsBySheet.mar;
|
||||
|
||||
const createSheet = (id: string, name: string, values: number[]) => {
|
||||
const cellData: Record<number, Record<number, { v: number | string; t: CellValueType }>> = {
|
||||
1: {
|
||||
1: { v: values[0], t: CellValueType.NUMBER },
|
||||
2: { v: values[1], t: CellValueType.NUMBER },
|
||||
},
|
||||
2: {
|
||||
1: { v: values[2], t: CellValueType.NUMBER },
|
||||
2: { v: values[3], t: CellValueType.NUMBER },
|
||||
},
|
||||
};
|
||||
sampleRowsBySheet[id]?.forEach((row, rowIndex) => {
|
||||
cellData[rowIndex + 4] = Object.fromEntries(
|
||||
row.map((value, columnIndex) => [
|
||||
columnIndex + 1,
|
||||
{
|
||||
v: value,
|
||||
t: typeof value === 'number' ? CellValueType.NUMBER : CellValueType.STRING,
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '3D reference',
|
||||
sheetOrder: ['jan', 'feb', 'mar', 'sheet1', 'sheet2', 'sheet3', 'result'],
|
||||
sheets: {
|
||||
jan: createSheet('jan', 'Jan', [1, 2, 3, 4]),
|
||||
feb: createSheet('feb', 'Feb', [10, 20, 30, 40]),
|
||||
mar: createSheet('mar', 'Mar', [100, 200, 300, 400]),
|
||||
sheet1: createSheet('sheet1', 'Sheet1', [0, 0, 0, 0]),
|
||||
sheet2: createSheet('sheet2', 'Sheet2', [0, 0, 0, 0]),
|
||||
sheet3: createSheet('sheet3', 'Sheet3', [0, 0, 0, 0]),
|
||||
result: createSheet('result', 'Result', [0, 0, 0, 0]),
|
||||
},
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
describe('three-dimensional sheet reference', () => {
|
||||
let calculate: (formula: string) => string | number | boolean | null | (string | number | boolean | null)[][];
|
||||
let currentConfigService: IFormulaCurrentConfigService;
|
||||
let dependencyGenerator: IFormulaDependencyGenerator;
|
||||
let dependencyManager: CapturingDependencyManagerService;
|
||||
let unitId: string;
|
||||
let activeSheetId: string;
|
||||
let sheetData: ISheetData;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFunctionTestBed(createWorkbookData(), [
|
||||
[IFeatureCalculationManagerService, { useClass: FeatureCalculationManagerService }],
|
||||
[IDependencyManagerService, { useClass: CapturingDependencyManagerService }],
|
||||
]);
|
||||
const lexer = testBed.get(Lexer);
|
||||
const astTreeBuilder = testBed.get(AstTreeBuilder);
|
||||
const interpreter = testBed.get(Interpreter);
|
||||
currentConfigService = testBed.get(IFormulaCurrentConfigService);
|
||||
dependencyGenerator = testBed.get(IFormulaDependencyGenerator);
|
||||
const manager = testBed.get(IDependencyManagerService);
|
||||
if (!(manager instanceof CapturingDependencyManagerService)) {
|
||||
throw new TypeError('Expected CapturingDependencyManagerService');
|
||||
}
|
||||
dependencyManager = manager;
|
||||
const runtimeService = testBed.get(IFormulaRuntimeService);
|
||||
unitId = testBed.unitId;
|
||||
activeSheetId = testBed.sheetId;
|
||||
sheetData = testBed.sheetData;
|
||||
|
||||
currentConfigService.load({
|
||||
formulaData: {},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: {
|
||||
[testBed.unitId]: testBed.sheetData,
|
||||
},
|
||||
});
|
||||
|
||||
const activeSheet = testBed.sheetData[testBed.sheetId];
|
||||
runtimeService.setCurrent(
|
||||
0,
|
||||
0,
|
||||
activeSheet.rowCount,
|
||||
activeSheet.columnCount,
|
||||
testBed.sheetId,
|
||||
testBed.unitId
|
||||
);
|
||||
testBed.get(IFunctionService).registerExecutors(
|
||||
new Sum(FUNCTION_NAMES_MATH.SUM),
|
||||
new Let(FUNCTION_NAMES_LOGICAL.LET),
|
||||
new Vstack(FUNCTION_NAMES_LOOKUP.VSTACK),
|
||||
new Filter(FUNCTION_NAMES_LOOKUP.FILTER),
|
||||
new Choosecols(FUNCTION_NAMES_LOOKUP.CHOOSECOLS),
|
||||
new Compare(FUNCTION_NAMES_META.COMPARE)
|
||||
);
|
||||
|
||||
calculate = (formula: string) => {
|
||||
const lexerNode = lexer.treeBuilder(formula);
|
||||
if (!(lexerNode instanceof LexerNode)) {
|
||||
throw new TypeError(`Failed to parse formula: ${formula}`);
|
||||
}
|
||||
const astNode = astTreeBuilder.parse(lexerNode);
|
||||
if (!(astNode instanceof BaseAstNode)) {
|
||||
throw new TypeError(`Failed to build AST: ${formula}`);
|
||||
}
|
||||
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
|
||||
return getObjectValue(result);
|
||||
};
|
||||
});
|
||||
|
||||
it('sums the same cell from every sheet in the inclusive range', () => {
|
||||
expect(calculate('=SUM(Jan:Mar!B2)')).toBe(111);
|
||||
expect(calculate("=SUM('Jan':'Mar'!B2)")).toBe(111);
|
||||
});
|
||||
|
||||
it('sums every cell from every sheet instead of only each area first cell', () => {
|
||||
expect(calculate('=SUM(Jan:Mar!B2:C3)')).toBe(1110);
|
||||
});
|
||||
|
||||
it('calculates the combine-data sample formula with all 12 source rows', () => {
|
||||
expect(calculate('=LET(data,VSTACK(Sheet1:Sheet3!B5:E16),FILTER(data,CHOOSECOLS(data,1)<>""))')).toEqual([
|
||||
[45148, 'Red', 1, 9],
|
||||
[45166, 'Blue', 7, 70],
|
||||
[45163, 'Green', 2, 16],
|
||||
[45174, 'Purple', 6, 72],
|
||||
[45177, 'Red', 2, 18],
|
||||
[45184, 'Purple', 2, 24],
|
||||
[45188, 'Green', 6, 48],
|
||||
[45194, 'Purple', 1, 12],
|
||||
[45200, 'Blue', 5, 50],
|
||||
[45205, 'Red', 4, 28],
|
||||
[45210, 'Green', 4, 32],
|
||||
[45213, 'Purple', 3, 36],
|
||||
]);
|
||||
});
|
||||
|
||||
it('tracks every sheet in the range as a dependency', async () => {
|
||||
currentConfigService.load({
|
||||
formulaData: {
|
||||
[unitId]: {
|
||||
[activeSheetId]: {
|
||||
0: {
|
||||
0: { f: '=SUM(Jan:Mar!B2)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayFormulaCellData: {},
|
||||
arrayFormulaRange: {},
|
||||
forceCalculate: false,
|
||||
dirtyRanges: [{
|
||||
unitId,
|
||||
sheetId: activeSheetId,
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
|
||||
}],
|
||||
dirtyNameMap: {},
|
||||
dirtyDefinedNameMap: {},
|
||||
dirtyUnitFeatureMap: {},
|
||||
dirtyUnitOtherFormulaMap: {},
|
||||
excludedCell: {},
|
||||
allUnitData: {
|
||||
[unitId]: sheetData,
|
||||
},
|
||||
});
|
||||
|
||||
const trees = await dependencyGenerator.generate();
|
||||
const tree = trees.find(
|
||||
(item): item is FormulaDependencyTree =>
|
||||
item instanceof FormulaDependencyTree && item.formula === '=SUM(Jan:Mar!B2)'
|
||||
);
|
||||
|
||||
if (!tree) {
|
||||
throw new TypeError('Expected a dependency tree for the 3D reference');
|
||||
}
|
||||
expect(dependencyManager.capturedRanges.get(tree.treeId)?.map(({ sheetId }) => sheetId)).toEqual([
|
||||
'jan',
|
||||
'feb',
|
||||
'mar',
|
||||
]);
|
||||
});
|
||||
});
|
||||
94
packages/engine-formula/src/engine/ast-node/ast-root-node.ts
Normal file
94
packages/engine-formula/src/engine/ast-node/ast-root-node.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { ErrorType } from '../../basics/error-type';
|
||||
import { DEFAULT_TOKEN_TYPE_ROOT } from '../../basics/token-type';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
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';
|
||||
|
||||
export class AstRootNode extends BaseAstNode {
|
||||
override get nodeType() {
|
||||
return NodeType.ROOT;
|
||||
}
|
||||
|
||||
override execute() {
|
||||
const children = this.getChildren();
|
||||
|
||||
if (children.length > 1) {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.VALUE));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const node = children[0];
|
||||
// if (node.nodeType === NodeType.FUNCTION) {
|
||||
// await node.executeAsync(interpreterCalculateProps);
|
||||
// } else {
|
||||
// node.execute(interpreterCalculateProps);
|
||||
// }
|
||||
if (node == null) {
|
||||
/**
|
||||
* fix: https://github.com/dream-num/univer/issues/1415
|
||||
*/
|
||||
this.setValue(ErrorValueObject.create(ErrorType.VALUE));
|
||||
} else {
|
||||
this.setValue(node.getValue());
|
||||
}
|
||||
|
||||
// return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
}
|
||||
}
|
||||
// export class AstVariantNode extends BaseAstNode {
|
||||
// get nodeType() {
|
||||
// return NodeType.Variant;
|
||||
// }
|
||||
|
||||
// async executeAsync(interpreterCalculateProps: IInterpreterCalculateProps) {
|
||||
// const children = this.getChildren();
|
||||
// const childrenCount = children.length;
|
||||
// for (let i = 0; i < childrenCount; i++) {
|
||||
// const node = children[i];
|
||||
// if (node.nodeType === NodeType.FUNCTION) {
|
||||
// await node.executeAsync(interpreterCalculateProps);
|
||||
// } else {
|
||||
// node.execute(interpreterCalculateProps);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
// }
|
||||
// }
|
||||
|
||||
export class AstRootNodeFactory extends BaseAstNodeFactory {
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.ROOT) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (!(param instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
const token = param.getToken();
|
||||
if (token === DEFAULT_TOKEN_TYPE_ROOT) {
|
||||
return new AstRootNode(DEFAULT_TOKEN_TYPE_ROOT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FORMULA_AST_NODE_REGISTRY.add(new AstRootNodeFactory());
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { BaseAstNode } from './base-ast-node';
|
||||
|
||||
export const DEFAULT_AST_NODE_FACTORY_Z_INDEX = 100;
|
||||
|
||||
export abstract class BaseAstNodeFactory {
|
||||
get zIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
}
|
||||
|
||||
create(param: LexerNode | string, currentRow?: number, currentColumn?: number): BaseAstNode {
|
||||
let token;
|
||||
if (param instanceof LexerNode) {
|
||||
token = param.getToken();
|
||||
} else {
|
||||
token = param;
|
||||
}
|
||||
return new BaseAstNode(token);
|
||||
}
|
||||
|
||||
abstract checkAndCreateNodeType(param: LexerNode | string): Nullable<BaseAstNode>;
|
||||
}
|
||||
214
packages/engine-formula/src/engine/ast-node/base-ast-node.ts
Normal file
214
packages/engine-formula/src/engine/ast-node/base-ast-node.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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 { DateSystem, Nullable } from '@univerjs/core';
|
||||
import type { ErrorType } from '../../basics/error-type';
|
||||
import type { FunctionVariantType } from '../reference-object/base-reference-object';
|
||||
import { AstNodePromiseType } from '../../basics/common';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { NodeType } from './node-type';
|
||||
|
||||
export interface IAstNodeNodeJson {
|
||||
token: string;
|
||||
children?: IAstNodeNodeJson[];
|
||||
nodeType: number;
|
||||
}
|
||||
|
||||
export type LambdaPrivacyVarType = Map<string, Nullable<BaseAstNode>>;
|
||||
|
||||
export class BaseAstNode {
|
||||
private _children: BaseAstNode[] = [];
|
||||
|
||||
private _definedNames: Nullable<Array<string>>;
|
||||
|
||||
private _parent: Nullable<BaseAstNode>;
|
||||
|
||||
private _valueObject: Nullable<FunctionVariantType>;
|
||||
|
||||
private _calculateState = false;
|
||||
|
||||
private _async = false;
|
||||
|
||||
private _address = false;
|
||||
|
||||
private _isForcedCalculateFunction = false;
|
||||
|
||||
constructor(private _token: string) {
|
||||
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._children.forEach((node) => {
|
||||
node.dispose();
|
||||
});
|
||||
this._valueObject?.dispose();
|
||||
|
||||
this._valueObject = null;
|
||||
|
||||
this._children = [];
|
||||
|
||||
this._definedNames = null;
|
||||
|
||||
this._parent = null;
|
||||
}
|
||||
|
||||
get nodeType() {
|
||||
return NodeType.BASE;
|
||||
}
|
||||
|
||||
resetCalculationState() {
|
||||
this._children.forEach((node) => {
|
||||
node.resetCalculationState();
|
||||
});
|
||||
this._valueObject = null;
|
||||
this._calculateState = false;
|
||||
}
|
||||
|
||||
isAsync() {
|
||||
return this._async;
|
||||
}
|
||||
|
||||
isAddress() {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
isForcedCalculateFunction() {
|
||||
return this._isForcedCalculateFunction;
|
||||
}
|
||||
|
||||
setAsync() {
|
||||
this._async = true;
|
||||
}
|
||||
|
||||
setAddress() {
|
||||
this._address = true;
|
||||
}
|
||||
|
||||
getParent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
setParent(node: BaseAstNode) {
|
||||
this._parent = node;
|
||||
node.addChildren(this);
|
||||
}
|
||||
|
||||
setForcedCalculateFunction() {
|
||||
this._isForcedCalculateFunction = true;
|
||||
}
|
||||
|
||||
getChildren() {
|
||||
return this._children;
|
||||
}
|
||||
|
||||
addChildren(...astNode: BaseAstNode[]) {
|
||||
this._children.push(...astNode);
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this._token;
|
||||
}
|
||||
|
||||
setValue(value: Nullable<FunctionVariantType>) {
|
||||
this._valueObject = value;
|
||||
}
|
||||
|
||||
getValue(): Nullable<FunctionVariantType> {
|
||||
return this._valueObject;
|
||||
}
|
||||
|
||||
preservesLazyIfReferenceArray(_child: BaseAstNode): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isCalculated() {
|
||||
return this._calculateState;
|
||||
}
|
||||
|
||||
setCalculated() {
|
||||
this._calculateState = true;
|
||||
}
|
||||
|
||||
execute(_dateSystem?: DateSystem) {
|
||||
/* abstract */
|
||||
}
|
||||
|
||||
setNotEmpty(state = true) {
|
||||
/* abstract */
|
||||
}
|
||||
|
||||
async executeAsync(_dateSystem?: DateSystem): Promise<AstNodePromiseType> {
|
||||
/* abstract */
|
||||
return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const token = this.getToken();
|
||||
const children = this.getChildren();
|
||||
|
||||
const childrenSerialization: IAstNodeNodeJson[] = [];
|
||||
const childrenCount = children.length;
|
||||
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
childrenSerialization.push(item.serialize());
|
||||
}
|
||||
|
||||
const result: IAstNodeNodeJson = {
|
||||
token,
|
||||
nodeType: this.nodeType,
|
||||
};
|
||||
|
||||
if (childrenCount > 0) {
|
||||
result.children = childrenSerialization;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
hasDefinedName(definedName: string) {
|
||||
return this._definedNames?.includes(definedName) || false;
|
||||
}
|
||||
|
||||
setDefinedNames(definedNames: Array<string>) {
|
||||
this._definedNames = definedNames;
|
||||
}
|
||||
|
||||
getDefinedNames() {
|
||||
return this._definedNames;
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorNode extends BaseAstNode {
|
||||
private _errorValueObject: ErrorValueObject;
|
||||
|
||||
constructor(errorType: ErrorType) {
|
||||
super(errorType);
|
||||
this._errorValueObject = ErrorValueObject.create(errorType);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.ERROR;
|
||||
}
|
||||
|
||||
static create(errorType: ErrorType) {
|
||||
return new ErrorNode(errorType);
|
||||
}
|
||||
|
||||
override getValue() {
|
||||
return this._errorValueObject;
|
||||
}
|
||||
}
|
||||
565
packages/engine-formula/src/engine/ast-node/function-node.ts
Normal file
565
packages/engine-formula/src/engine/ast-node/function-node.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* 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 { BaseFunction } from '../../functions/base-function';
|
||||
import type { LexerNode } from '../analysis/lexer-node';
|
||||
import type {
|
||||
AsyncArrayObject,
|
||||
AsyncObject,
|
||||
BaseReferenceObject,
|
||||
FunctionVariantType,
|
||||
NodeValueType,
|
||||
} from '../reference-object/base-reference-object';
|
||||
import type { BaseValueObject } from '../value-object/base-value-object';
|
||||
import type { FormulaFunctionResultValueType } from '../value-object/primitive-object';
|
||||
import { DateSystem, Inject, Injector } from '@univerjs/core';
|
||||
import { AstNodePromiseType } from '../../basics/common';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { matchToken } from '../../basics/token';
|
||||
import { COLUMN_LIKE_FUNCTION_NAMES } from '../../functions/column-like-functions';
|
||||
import { FUNCTION_NAMES_TEXT } from '../../functions/text/function-names';
|
||||
import { FormulaDataModel } from '../../models/formula-data.model';
|
||||
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
|
||||
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';
|
||||
import { BaseAstNode, ErrorNode } 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';
|
||||
|
||||
export class FunctionNode extends BaseAstNode {
|
||||
constructor(
|
||||
token: string,
|
||||
private _functionExecutor: BaseFunction,
|
||||
private _currentConfigService: IFormulaCurrentConfigService,
|
||||
private _runtimeService: IFormulaRuntimeService,
|
||||
private _definedNamesService: IDefinedNamesService,
|
||||
private _formulaDataModel: FormulaDataModel,
|
||||
private _unitReferenceResolver: IFormulaUnitReferenceResolver
|
||||
) {
|
||||
super(token);
|
||||
|
||||
if (this._functionExecutor.isAsync()) {
|
||||
this.setAsync();
|
||||
}
|
||||
|
||||
if (this._functionExecutor.isAddress()) {
|
||||
this.setAddress();
|
||||
}
|
||||
|
||||
if (this._functionExecutor.needsLocale) {
|
||||
this._setLocale();
|
||||
}
|
||||
|
||||
if (this._functionExecutor.needsSheetsInfo) {
|
||||
this._setSheetsInfo();
|
||||
}
|
||||
|
||||
if (this._functionExecutor.needsFormulaDataModel) {
|
||||
this._functionExecutor.setFormulaDataModel(this._formulaDataModel);
|
||||
}
|
||||
|
||||
if (this._functionExecutor.needsUnitReferenceResolver) {
|
||||
this._functionExecutor.setUnitReferenceResolver(this._unitReferenceResolver);
|
||||
}
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.FUNCTION;
|
||||
}
|
||||
|
||||
override preservesLazyIfReferenceArray(child: BaseAstNode): boolean {
|
||||
return this._functionExecutor.lazyIfReferenceArrayArgumentIndexes.includes(this.getChildren().indexOf(child));
|
||||
}
|
||||
|
||||
override async executeAsync(dateSystem: DateSystem = DateSystem.Date1900) {
|
||||
const children = this.getChildren();
|
||||
|
||||
this._compatibility();
|
||||
|
||||
const resultVariant = this._functionExecutor.needsAstChildren
|
||||
? this._calculateAst(children, dateSystem)
|
||||
: await this._calculateAsync(this._collectVariants(children, dateSystem), dateSystem);
|
||||
let result: FunctionVariantType;
|
||||
|
||||
if (resultVariant.isAsyncObject() || resultVariant.isAsyncArrayObject()) {
|
||||
result = await (resultVariant as AsyncObject | AsyncArrayObject).getValue() as FunctionVariantType;
|
||||
} else {
|
||||
result = resultVariant as FunctionVariantType;
|
||||
}
|
||||
|
||||
result = this._normalizeTopLevelLegacyArrayResult(result).withDateSystem(dateSystem) as FunctionVariantType;
|
||||
|
||||
this._setEmbeddedArrayFormulaToResult(result);
|
||||
|
||||
this._setRefData(result);
|
||||
|
||||
this.setValue(result);
|
||||
|
||||
return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
}
|
||||
|
||||
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
|
||||
const children = this.getChildren();
|
||||
|
||||
this._compatibility();
|
||||
|
||||
const resultVariant = (this._functionExecutor.needsAstChildren
|
||||
? this._calculateAst(children, dateSystem)
|
||||
: this._calculate(this._collectVariants(children, dateSystem), dateSystem)) as FunctionVariantType;
|
||||
|
||||
const result = this._normalizeTopLevelLegacyArrayResult(resultVariant).withDateSystem(dateSystem) as FunctionVariantType;
|
||||
|
||||
this._setEmbeddedArrayFormulaToResult(result);
|
||||
|
||||
this._setRefData(result);
|
||||
|
||||
this.setValue(result as FunctionVariantType);
|
||||
}
|
||||
|
||||
private _collectVariants(children: BaseAstNode[], dateSystem: DateSystem): BaseValueObject[] {
|
||||
const variants: BaseValueObject[] = [];
|
||||
const childrenCount = children.length;
|
||||
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const object = this._getChildVariant(children[i], dateSystem);
|
||||
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
variants.push(object as BaseValueObject);
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
|
||||
private _getChildVariant(child: BaseAstNode, dateSystem: DateSystem): FunctionVariantType | undefined {
|
||||
const object = child.getValue();
|
||||
|
||||
if (object == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (object.isReferenceObject() && this._functionExecutor.needsFilteredOutRows) {
|
||||
this._setFilteredOutRows(object as BaseReferenceObject);
|
||||
}
|
||||
|
||||
// In the SUBTOTAL function, we need to get rowData information, we can only use ReferenceObject
|
||||
if (object.isReferenceObject() && !this._functionExecutor.needsReferenceObject) {
|
||||
// Array converted from reference object needs to be marked
|
||||
return (object as BaseReferenceObject).withDateSystem(dateSystem).toArrayValueObject();
|
||||
}
|
||||
|
||||
return object.withDateSystem(dateSystem);
|
||||
}
|
||||
|
||||
isFunctionExecutorArgumentsIgnoreNumberPattern() {
|
||||
return this._functionExecutor.isArgumentsIgnoreNumberPattern();
|
||||
}
|
||||
|
||||
/**
|
||||
* If it contains an array formula, set the current cell to the cache and send itself as a ref outward
|
||||
*/
|
||||
private _setEmbeddedArrayFormulaToResult(result: FunctionVariantType) {
|
||||
if (!result.isArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rowCount = (result as ArrayValueObject).getRowCount();
|
||||
const colCount = (result as ArrayValueObject).getColumnCount();
|
||||
|
||||
if (rowCount <= 1 && colCount <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._runtimeService.setUnitArrayFormulaEmbeddedMap();
|
||||
}
|
||||
|
||||
private _normalizeTopLevelLegacyArrayResult(result: FunctionVariantType): FunctionVariantType {
|
||||
const parent = this.getParent();
|
||||
const isTopLevel = parent == null || parent.nodeType === NodeType.ROOT;
|
||||
if (!this._functionExecutor.returnsLegacyArrayAsScalar || !isTopLevel || !result.isArray()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const array = result as ArrayValueObject;
|
||||
if (array.getRowCount() <= 1 && array.getColumnCount() <= 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (
|
||||
this._runtimeService.hasFunctionRefInfoOverride() &&
|
||||
(this._runtimeService.currentRowCount > 1 || this._runtimeService.currentColumnCount > 1)
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return array.getFirstCell();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility handling for special functions.
|
||||
*/
|
||||
private _compatibility() {
|
||||
this._lookupCompatibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* The LOOKUP function follows the following rules when dealing with vectors of different sizes:
|
||||
* If the lookup_vector is larger than the result_vector,
|
||||
* the LOOKUP function will ignore the extra portion of the lookup_vector and only use the portion of the result_vector that is the same size as the lookup_vector for lookup and returning results.
|
||||
* If the lookup_vector is smaller than the result_vector,
|
||||
* the LOOKUP function will continue using the last value of the result_vector for lookup and returning results after the last value of the lookup_vector.
|
||||
*/
|
||||
private _lookupCompatibility() {
|
||||
const children = this.getChildren();
|
||||
const childrenCount = children.length;
|
||||
|
||||
if (!this._functionExecutor.needsExpandParams || childrenCount !== 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lookupVectorOrArray = children[1].getValue();
|
||||
|
||||
const resultVector = children[2].getValue();
|
||||
|
||||
if (!lookupVectorOrArray?.isReferenceObject() && !resultVector?.isReferenceObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lookupCountRow: number;
|
||||
let lookupCountColumn: number;
|
||||
|
||||
if (lookupVectorOrArray?.isReferenceObject()) {
|
||||
const lookupVectorOrArrayRange = (lookupVectorOrArray as BaseReferenceObject).getRangeData();
|
||||
const { startRow, startColumn, endRow, endColumn } = lookupVectorOrArrayRange;
|
||||
|
||||
lookupCountRow = endRow - startRow + 1;
|
||||
lookupCountColumn = endColumn - startColumn + 1;
|
||||
} else {
|
||||
lookupCountRow = lookupVectorOrArray?.isArray() ? (lookupVectorOrArray as ArrayValueObject).getRowCount() : 1;
|
||||
lookupCountColumn = lookupVectorOrArray?.isArray() ? (lookupVectorOrArray as ArrayValueObject).getColumnCount() : 1;
|
||||
}
|
||||
|
||||
const resultVectorRange = (resultVector as BaseReferenceObject).getRangeData();
|
||||
|
||||
const { startRow: reStartRow, startColumn: reStartColumn, endRow: reEndRow, endColumn: reEndColumn } = resultVectorRange;
|
||||
|
||||
const resultCountRow = reEndRow - reStartRow + 1;
|
||||
const resultCountColumn = reEndColumn - reStartColumn + 1;
|
||||
|
||||
if (lookupCountRow !== resultCountRow) {
|
||||
resultVectorRange.endRow += lookupCountRow - resultCountRow;
|
||||
}
|
||||
|
||||
if (lookupCountColumn !== resultCountColumn) {
|
||||
resultVectorRange.endColumn += lookupCountColumn - resultCountColumn;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the result of a custom function to a NodeValueType.
|
||||
*/
|
||||
private _handleCustomResult(
|
||||
resultVariantCustom: FormulaFunctionResultValueType,
|
||||
dateSystem: DateSystem
|
||||
): NodeValueType {
|
||||
if (typeof resultVariantCustom !== 'object' || resultVariantCustom == null) {
|
||||
return ValueObjectFactory.create(resultVariantCustom, false, dateSystem);
|
||||
}
|
||||
|
||||
const arrayValues = transformToValueObject(resultVariantCustom, false, dateSystem);
|
||||
return ArrayValueObject.create({
|
||||
calculateValueList: arrayValues,
|
||||
rowCount: arrayValues.length,
|
||||
columnCount: arrayValues[0]?.length || 0,
|
||||
unitId: '',
|
||||
sheetId: '',
|
||||
row: -1,
|
||||
column: -1,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAddressFunction() {
|
||||
/**
|
||||
* In Excel, to inject a defined name into a function that has positioning capabilities,
|
||||
* such as using the INDIRECT function to reference a named range,
|
||||
* you can write it as follows:
|
||||
* =INDIRECT("DefinedName1")
|
||||
*/
|
||||
if (this._functionExecutor.isAddress()) {
|
||||
this._setDefinedNamesForFunction();
|
||||
}
|
||||
}
|
||||
|
||||
private _mapVariantsToValues(variants: BaseValueObject[]) {
|
||||
return variants.map((variant) => {
|
||||
if (variant.isArray()) {
|
||||
return (variant as ArrayValueObject).toValue();
|
||||
}
|
||||
|
||||
if (variant.isLambda()) {
|
||||
return variant;
|
||||
}
|
||||
|
||||
return variant.getValue();
|
||||
});
|
||||
}
|
||||
|
||||
private _calculate(variants: BaseValueObject[], dateSystem: DateSystem) {
|
||||
// Check the number of parameters
|
||||
const { minParams, maxParams } = this._functionExecutor;
|
||||
if (minParams !== -1 && maxParams !== -1 && (variants.length < minParams || variants.length > maxParams)) {
|
||||
return ErrorValueObject.create(ErrorType.NA);
|
||||
}
|
||||
let resultVariant: NodeValueType;
|
||||
|
||||
this._setRefInfo();
|
||||
|
||||
if (this._functionExecutor.isCustom()) {
|
||||
const resultVariantCustom = this._functionExecutor.calculateCustom(
|
||||
...this._mapVariantsToValues(variants)
|
||||
) as FormulaFunctionResultValueType;
|
||||
|
||||
resultVariant = this._handleCustomResult(resultVariantCustom, dateSystem);
|
||||
} else {
|
||||
this._handleAddressFunction();
|
||||
resultVariant = this._functionExecutor.calculate(...variants);
|
||||
}
|
||||
|
||||
return resultVariant;
|
||||
}
|
||||
|
||||
private _calculateAst(children: BaseAstNode[], dateSystem: DateSystem) {
|
||||
const { minParams, maxParams } = this._functionExecutor;
|
||||
if (minParams !== -1 && maxParams !== -1 && (children.length < minParams || children.length > maxParams)) {
|
||||
return ErrorValueObject.create(ErrorType.NA);
|
||||
}
|
||||
|
||||
this._setRefInfo();
|
||||
this._handleAddressFunction();
|
||||
|
||||
return this._functionExecutor.calculateAst(children, (node) => this._getChildVariant(node, dateSystem) ?? null);
|
||||
}
|
||||
|
||||
private async _calculateAsync(variants: BaseValueObject[], dateSystem: DateSystem) {
|
||||
// Check the number of parameters
|
||||
const { minParams, maxParams } = this._functionExecutor;
|
||||
if (minParams !== -1 && maxParams !== -1 && (variants.length < minParams || variants.length > maxParams)) {
|
||||
return ErrorValueObject.create(ErrorType.NA);
|
||||
}
|
||||
|
||||
let resultVariant: NodeValueType;
|
||||
|
||||
this._setRefInfo();
|
||||
|
||||
if (this._functionExecutor.isCustom()) {
|
||||
const resultVariantCustom = await this._functionExecutor.calculateCustom(
|
||||
...this._mapVariantsToValues(variants)
|
||||
);
|
||||
|
||||
resultVariant = this._handleCustomResult(resultVariantCustom, dateSystem);
|
||||
} else {
|
||||
this._handleAddressFunction();
|
||||
resultVariant = this._functionExecutor.calculate(...variants);
|
||||
}
|
||||
|
||||
return resultVariant;
|
||||
}
|
||||
|
||||
private _setDefinedNamesForFunction() {
|
||||
const editorUnitId = this._runtimeService.currentUnitId;
|
||||
if (editorUnitId == null) {
|
||||
return;
|
||||
}
|
||||
const definedNames = this._definedNamesService.getDefinedNameMap(editorUnitId);
|
||||
if (definedNames == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._functionExecutor.setDefinedNames(definedNames);
|
||||
}
|
||||
|
||||
private _setRefInfo() {
|
||||
const { currentUnitId, currentSubUnitId, currentRow, currentColumn, currentRowCount, currentColumnCount } = this._runtimeService;
|
||||
|
||||
this._functionExecutor.setRefInfo(currentUnitId, currentSubUnitId, currentRow, currentColumn, currentRowCount, currentColumnCount);
|
||||
this._functionExecutor.setDateSystem(this._currentConfigService.getDateSystem(currentUnitId));
|
||||
|
||||
if (this._functionExecutor.needsSheetRowColumnCount) {
|
||||
const { rowCount, columnCount } = this._currentConfigService.getSheetRowColumnCount(currentUnitId, currentSubUnitId);
|
||||
|
||||
this._functionExecutor.setSheetRowColumnCount(rowCount, columnCount);
|
||||
}
|
||||
}
|
||||
|
||||
private _setRefData(variant: FunctionVariantType) {
|
||||
if (!variant.isReferenceObject()) {
|
||||
return;
|
||||
}
|
||||
const referenceObject = variant as BaseReferenceObject;
|
||||
|
||||
referenceObject.setForcedSheetId(this._currentConfigService.getSheetNameMap());
|
||||
|
||||
referenceObject.setUnitData(this._currentConfigService.getUnitData());
|
||||
|
||||
referenceObject.setArrayFormulaCellData(this._currentConfigService.getArrayFormulaCellData());
|
||||
|
||||
referenceObject.setArrayFormulaRange(this._currentConfigService.getArrayFormulaRange());
|
||||
|
||||
referenceObject.setRuntimeData(this._runtimeService.getUnitData());
|
||||
|
||||
referenceObject.setRuntimeArrayFormulaCellData(this._runtimeService.getRuntimeArrayFormulaCellData());
|
||||
|
||||
referenceObject.setRuntimeArrayFormulaRange(this._runtimeService.getUnitArrayFormula());
|
||||
|
||||
referenceObject.setRuntimeFeatureCellData(this._runtimeService.getRuntimeFeatureCellData());
|
||||
}
|
||||
|
||||
private _setLocale() {
|
||||
this._functionExecutor.setLocale(this._currentConfigService.getLocale());
|
||||
}
|
||||
|
||||
private _setSheetsInfo() {
|
||||
this._functionExecutor.setSheetsInfo(this._currentConfigService.getSheetsInfo());
|
||||
}
|
||||
|
||||
private _setFilteredOutRows(referenceObject: BaseReferenceObject) {
|
||||
const { startRow, endRow } = referenceObject.getRangePosition();
|
||||
const filteredOutRows = this._currentConfigService.getFilteredOutRows(
|
||||
referenceObject.getUnitId(),
|
||||
referenceObject.getSheetId(),
|
||||
startRow,
|
||||
endRow
|
||||
);
|
||||
|
||||
referenceObject.setFilteredOutRows(filteredOutRows);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorFunctionNode extends BaseAstNode {
|
||||
constructor(
|
||||
token: string = 'Error'
|
||||
) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.FUNCTION;
|
||||
}
|
||||
|
||||
override async executeAsync() {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.NAME) as FunctionVariantType);
|
||||
|
||||
return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
}
|
||||
|
||||
override execute() {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.NAME) as FunctionVariantType);
|
||||
}
|
||||
}
|
||||
|
||||
export class FunctionNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService,
|
||||
@IFormulaRuntimeService private readonly _runtimeService: IFormulaRuntimeService,
|
||||
@IDefinedNamesService private readonly _definedNamesService: IDefinedNamesService,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
|
||||
@IFormulaUnitReferenceResolver private readonly _unitReferenceResolver: IFormulaUnitReferenceResolver
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.FUNCTION) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override create(token: string): BaseAstNode {
|
||||
const functionExecutor = this._functionService.getExecutor(token);
|
||||
if (!functionExecutor) {
|
||||
console.error(`No function ${token}`);
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
|
||||
return new FunctionNode(
|
||||
token,
|
||||
functionExecutor,
|
||||
this._currentConfigService,
|
||||
this._runtimeService,
|
||||
this._definedNamesService,
|
||||
this._formulaDataModel,
|
||||
this._unitReferenceResolver
|
||||
);
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (typeof param === 'string') {
|
||||
return;
|
||||
}
|
||||
const token = param.getToken();
|
||||
|
||||
const { tokenTrim, minusPrefixNode, atPrefixNode } = prefixHandler(token.trim(), this._functionService, this._runtimeService);
|
||||
|
||||
const isColon = this._isParentUnionNode(param);
|
||||
|
||||
if (!Number.isNaN(Number(tokenTrim)) && !isColon) {
|
||||
return ErrorNode.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
const tokenTrimUpper = normalizeFunctionToken(tokenTrim.toUpperCase());
|
||||
|
||||
if (isColon && COLUMN_LIKE_FUNCTION_NAMES.has(tokenTrimUpper)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._functionService.hasExecutor(tokenTrimUpper)) {
|
||||
const functionNode = this.create(tokenTrimUpper);
|
||||
if (atPrefixNode) {
|
||||
functionNode.setParent(atPrefixNode);
|
||||
// return atPrefixNode;
|
||||
} else if (minusPrefixNode) {
|
||||
functionNode.setParent(minusPrefixNode);
|
||||
// return minusPrefixNode;
|
||||
}
|
||||
return functionNode;
|
||||
}
|
||||
}
|
||||
|
||||
private _isParentUnionNode(param: LexerNode) {
|
||||
return param.getParent()?.getParent()?.getToken() === matchToken.COLON;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeFunctionToken(token: string): string {
|
||||
if (token === 'REGEXTEST' || token === '_XLFN.REGEXTEST') {
|
||||
return FUNCTION_NAMES_TEXT.REGEXMATCH;
|
||||
}
|
||||
|
||||
for (const prefix of ['_XLFN.', '_XLWS.', '_XLETA.']) {
|
||||
if (token.startsWith(prefix)) {
|
||||
return token.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
26
packages/engine-formula/src/engine/ast-node/index.ts
Normal file
26
packages/engine-formula/src/engine/ast-node/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './ast-root-node';
|
||||
export * from './function-node';
|
||||
export * from './lambda-node';
|
||||
export * from './lambda-parameter-node';
|
||||
export * from './operator-node';
|
||||
export * from './prefix-node';
|
||||
export * from './reference-node';
|
||||
export * from './suffix-node';
|
||||
export * from './union-node';
|
||||
export * from './value-node';
|
||||
191
packages/engine-formula/src/engine/ast-node/lambda-node.ts
Normal file
191
packages/engine-formula/src/engine/ast-node/lambda-node.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { LambdaPrivacyVarType } from './base-ast-node';
|
||||
import { generateRandomId, Inject } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import {
|
||||
DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME,
|
||||
DEFAULT_TOKEN_TYPE_LAMBDA_OMIT_PARAMETER,
|
||||
DEFAULT_TOKEN_TYPE_LAMBDA_PARAMETER,
|
||||
} from '../../basics/token-type';
|
||||
import { IFormulaRuntimeService } from '../../services/runtime.service';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { Interpreter } from '../interpreter/interpreter';
|
||||
import { updateLambdaStatement } from '../utils/update-lambda-statement';
|
||||
import { LambdaValueObjectObject } from '../value-object/lambda-value-object';
|
||||
import { BaseAstNode, ErrorNode } 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';
|
||||
|
||||
export class LambdaNode extends BaseAstNode {
|
||||
private _isNotEmpty = true;
|
||||
|
||||
constructor(
|
||||
token: string,
|
||||
private _lambdaId: string,
|
||||
private _interpreter: Interpreter,
|
||||
private _lambdaPrivacyVarKeys: string[],
|
||||
private _runtimeService: IFormulaRuntimeService
|
||||
) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.LAMBDA;
|
||||
}
|
||||
|
||||
override setNotEmpty(state = false) {
|
||||
this._isNotEmpty = state;
|
||||
}
|
||||
|
||||
isEmptyParamFunction() {
|
||||
return this.getChildren().length < 2 && this._isNotEmpty;
|
||||
}
|
||||
|
||||
isFunctionParameter() {
|
||||
return this._lambdaId === null;
|
||||
}
|
||||
|
||||
getLambdaId() {
|
||||
return this._lambdaId;
|
||||
}
|
||||
|
||||
override execute() {
|
||||
if (this.isEmptyParamFunction()) {
|
||||
this.setValue(LambdaValueObjectObject.create(this, this._interpreter, this._lambdaPrivacyVarKeys));
|
||||
} else {
|
||||
const children = this.getChildren();
|
||||
const childrenCount = children.length;
|
||||
this.setValue(children[childrenCount - 1].getValue());
|
||||
}
|
||||
|
||||
this._runtimeService.setUnitArrayFormulaEmbeddedMap();
|
||||
}
|
||||
|
||||
// override async executeAsync() {
|
||||
// if (this.isEmptyParamFunction()) {
|
||||
// await this.setValue(LambdaValueObjectObject.create(this, this._interpreter, this._lambdaPrivacyVarKeys));
|
||||
// } else {
|
||||
// const children = this.getChildren();
|
||||
// const childrenCount = children.length;
|
||||
// await this.setValue(children[childrenCount - 1].getValue());
|
||||
// }
|
||||
|
||||
// return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
// }
|
||||
}
|
||||
|
||||
export class LambdaNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFormulaRuntimeService private readonly _runtimeService: IFormulaRuntimeService,
|
||||
@Inject(Interpreter) private readonly _interpreter: Interpreter
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.LAMBDA) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override create(param: LexerNode): BaseAstNode {
|
||||
const children = param.getChildren();
|
||||
const lambdaVar = children[0];
|
||||
let parameterArray = children.slice(1, -1);
|
||||
const functionStatementNode = children[children.length - 1];
|
||||
if (!(lambdaVar instanceof LexerNode && functionStatementNode instanceof LexerNode)) {
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
|
||||
if (lambdaVar.getToken() === DEFAULT_TOKEN_TYPE_LAMBDA_PARAMETER) {
|
||||
const lambdaVarChildren = lambdaVar.getChildren();
|
||||
|
||||
if (parameterArray.length !== lambdaVarChildren.length) {
|
||||
return ErrorNode.create(ErrorType.VALUE);
|
||||
}
|
||||
} else {
|
||||
parameterArray = children.slice(0, -1);
|
||||
}
|
||||
|
||||
// const lambdaId = nanoid(8);
|
||||
const lambdaId = generateRandomId(8);
|
||||
|
||||
// const lambdaRuntime = parserDataLoader.getLambdaRuntime();
|
||||
const currentLambdaPrivacyVar = new Map<string, Nullable<BaseAstNode>>();
|
||||
|
||||
for (let i = 0; i < parameterArray.length; i++) {
|
||||
const parameter = parameterArray[i];
|
||||
if (parameter instanceof LexerNode) {
|
||||
const variant = getLambdaParameterName(parameter);
|
||||
if (variant == null) {
|
||||
return ErrorNode.create(ErrorType.VALUE);
|
||||
}
|
||||
parameter.setToken(DEFAULT_TOKEN_TYPE_LAMBDA_OMIT_PARAMETER);
|
||||
currentLambdaPrivacyVar.set(variant, undefined);
|
||||
} else {
|
||||
return ErrorNode.create(ErrorType.VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
this._runtimeService.registerFunctionDefinitionPrivacyVar(lambdaId, currentLambdaPrivacyVar);
|
||||
|
||||
this._updateLambdaStatement(functionStatementNode, lambdaId, currentLambdaPrivacyVar);
|
||||
|
||||
return new LambdaNode(param.getToken(), lambdaId, this._interpreter, [...currentLambdaPrivacyVar.keys()], this._runtimeService);
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (!(param instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = param.getToken().trim().toUpperCase();
|
||||
if (token !== DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.create(param);
|
||||
}
|
||||
|
||||
private _updateLambdaStatement(
|
||||
functionStatementNode: LexerNode,
|
||||
lambdaId: string,
|
||||
currentLambdaPrivacyVar: LambdaPrivacyVarType
|
||||
) {
|
||||
updateLambdaStatement(functionStatementNode, lambdaId, currentLambdaPrivacyVar);
|
||||
}
|
||||
}
|
||||
|
||||
function getLambdaParameterName(parameter: LexerNode): Nullable<string> {
|
||||
const children = parameter.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
if (typeof child === 'string') {
|
||||
const token = child.trim();
|
||||
if (token.length > 0) {
|
||||
return token;
|
||||
}
|
||||
} else {
|
||||
const token = getLambdaParameterName(child);
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
|
||||
import type { LambdaPrivacyVarType } from './base-ast-node';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { DEFAULT_TOKEN_TYPE_LAMBDA_RUNTIME_PARAMETER } from '../../basics/token-type';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BaseAstNode, ErrorNode } 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';
|
||||
|
||||
function getRootLexerNode(node: Nullable<BaseAstNode>): Nullable<BaseAstNode> {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (node.getToken() !== DEFAULT_TOKEN_TYPE_LAMBDA_RUNTIME_PARAMETER) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const parameterNode = node as LambdaParameterNode;
|
||||
const currentLambdaPrivacyVar = parameterNode.getCurrentLambdaPrivacyVar();
|
||||
const lambdaParameter = parameterNode.getLambdaParameter();
|
||||
|
||||
if (!currentLambdaPrivacyVar) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getRootLexerNode(currentLambdaPrivacyVar.get(lambdaParameter));
|
||||
}
|
||||
|
||||
export class LambdaParameterNode extends BaseAstNode {
|
||||
constructor(
|
||||
token: string,
|
||||
private _lambdaParameter: string,
|
||||
private _currentLambdaPrivacyVar: LambdaPrivacyVarType
|
||||
) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
getLambdaParameter() {
|
||||
return this._lambdaParameter;
|
||||
}
|
||||
|
||||
getCurrentLambdaPrivacyVar() {
|
||||
return this._currentLambdaPrivacyVar;
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.LAMBDA_PARAMETER;
|
||||
}
|
||||
|
||||
override execute() {
|
||||
/**
|
||||
* In the case of =lambda(x,y,lambda(a,b,a*b)(x,y))(1,2)
|
||||
*/
|
||||
const node = getRootLexerNode(this._currentLambdaPrivacyVar.get(this._lambdaParameter));
|
||||
if (!node) {
|
||||
const value = this.getValue();
|
||||
if (value == null || value.isError()) {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.NAME));
|
||||
}
|
||||
} else {
|
||||
this.setValue(node.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LambdaParameterNodeFactory extends BaseAstNodeFactory {
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.LAMBDA_PARAMETER) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override create(param: LexerNode): BaseAstNode {
|
||||
// const lambdaId = param.getLambdaId();
|
||||
const currentLambdaPrivacyVar = param.getFunctionDefinitionPrivacyVar();
|
||||
const lambdaParameter = param.getLambdaParameter();
|
||||
|
||||
if (!currentLambdaPrivacyVar) {
|
||||
return new ErrorNode(ErrorType.NAME);
|
||||
}
|
||||
|
||||
return new LambdaParameterNode(param.getToken(), lambdaParameter, currentLambdaPrivacyVar);
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (!(param instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = param.getToken().trim();
|
||||
if (token !== DEFAULT_TOKEN_TYPE_LAMBDA_RUNTIME_PARAMETER) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.create(param);
|
||||
}
|
||||
}
|
||||
44
packages/engine-formula/src/engine/ast-node/node-type.ts
Normal file
44
packages/engine-formula/src/engine/ast-node/node-type.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum NodeType {
|
||||
REFERENCE = 1,
|
||||
VALUE = 2,
|
||||
OPERATOR = 3,
|
||||
FUNCTION = 4,
|
||||
LAMBDA = 5,
|
||||
LAMBDA_PARAMETER = 6,
|
||||
ERROR = 7,
|
||||
BASE = 8,
|
||||
ROOT = 9,
|
||||
UNION = 10,
|
||||
PREFIX = 11,
|
||||
SUFFIX = 12,
|
||||
NULL = 13,
|
||||
}
|
||||
|
||||
export const NODE_ORDER_MAP = new Map([
|
||||
[NodeType.REFERENCE, 7],
|
||||
[NodeType.VALUE, 9],
|
||||
[NodeType.OPERATOR, 8],
|
||||
[NodeType.FUNCTION, 6],
|
||||
[NodeType.LAMBDA, 1],
|
||||
[NodeType.LAMBDA_PARAMETER, 2],
|
||||
[NodeType.ROOT, 10],
|
||||
[NodeType.UNION, 3],
|
||||
[NodeType.PREFIX, 4],
|
||||
[NodeType.SUFFIX, 5],
|
||||
]);
|
||||
33
packages/engine-formula/src/engine/ast-node/null-node.ts
Normal file
33
packages/engine-formula/src/engine/ast-node/null-node.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { NullValueObject } from '../value-object/primitive-object';
|
||||
import { BaseAstNode } from './base-ast-node';
|
||||
import { NodeType } from './node-type';
|
||||
|
||||
export class NullNode extends BaseAstNode {
|
||||
constructor(private _operatorString: string) {
|
||||
super(_operatorString);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.NULL;
|
||||
}
|
||||
|
||||
override execute() {
|
||||
this.setValue(NullValueObject.create());
|
||||
}
|
||||
}
|
||||
179
packages/engine-formula/src/engine/ast-node/operator-node.ts
Normal file
179
packages/engine-formula/src/engine/ast-node/operator-node.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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 { compareToken } from '../../basics/token';
|
||||
import type { BaseFunction } from '../../functions/base-function';
|
||||
import type { Compare } from '../../functions/meta/compare';
|
||||
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
|
||||
import type { ArrayValueObject } from '../value-object/array-value-object';
|
||||
import type { BaseValueObject } from '../value-object/base-value-object';
|
||||
import { DateSystem } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { OPERATOR_TOKEN_COMPARE_SET, OPERATOR_TOKEN_SET, operatorToken } from '../../basics/token';
|
||||
import { FUNCTION_NAMES_MATH } from '../../functions/math/function-names';
|
||||
import { FUNCTION_NAMES_META } from '../../functions/meta/function-names';
|
||||
import { FUNCTION_NAMES_TEXT } from '../../functions/text/function-names';
|
||||
import { IFunctionService } from '../../services/function.service';
|
||||
import { IFormulaRuntimeService } from '../../services/runtime.service';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { NullValueObject } from '../value-object/primitive-object';
|
||||
import { BaseAstNode, ErrorNode } 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';
|
||||
|
||||
export class OperatorNode extends BaseAstNode {
|
||||
constructor(
|
||||
operatorString: string,
|
||||
private _functionExecutor: BaseFunction,
|
||||
private _runtimeService: IFormulaRuntimeService
|
||||
) {
|
||||
super(operatorString);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.OPERATOR;
|
||||
}
|
||||
|
||||
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
|
||||
const children = this.getChildren();
|
||||
if (this._functionExecutor.name === FUNCTION_NAMES_META.COMPARE) {
|
||||
(this._functionExecutor as Compare).setCompareType(this.getToken() as compareToken);
|
||||
}
|
||||
|
||||
const child1 = children[0];
|
||||
const child2 = children[1];
|
||||
|
||||
let object1 = child1?.getValue();
|
||||
let object2 = child2?.getValue();
|
||||
|
||||
const token = this.getToken();
|
||||
|
||||
if ((object1 == null || object2 == null) && token !== operatorToken.MINUS && token !== operatorToken.PLUS) {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.VALUE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (object1 == null) {
|
||||
object1 = NullValueObject.create();
|
||||
}
|
||||
|
||||
if (object2 == null) {
|
||||
object2 = NullValueObject.create();
|
||||
}
|
||||
|
||||
if (object1.isReferenceObject()) {
|
||||
object1 = this._referenceToOperatorValue(object1 as BaseReferenceObject);
|
||||
}
|
||||
|
||||
if (object2.isReferenceObject()) {
|
||||
object2 = this._referenceToOperatorValue(object2 as BaseReferenceObject);
|
||||
}
|
||||
|
||||
object1 = object1.withDateSystem(dateSystem);
|
||||
object2 = object2.withDateSystem(dateSystem);
|
||||
const result = this._functionExecutor.calculate(
|
||||
object1 as BaseValueObject,
|
||||
object2 as BaseValueObject
|
||||
) as FunctionVariantType;
|
||||
|
||||
this._setEmbeddedArrayFormulaToResult(result);
|
||||
|
||||
this.setValue(result.withDateSystem(dateSystem));
|
||||
}
|
||||
|
||||
private _referenceToOperatorValue(reference: BaseReferenceObject): BaseValueObject {
|
||||
const array = reference.toArrayValueObject();
|
||||
if (array.getRowCount() === 1 && array.getColumnCount() === 1) {
|
||||
return array.get(0, 0) as BaseValueObject;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* If it contains an array formula, set the current cell to the cache and send itself as a ref outward
|
||||
*/
|
||||
private _setEmbeddedArrayFormulaToResult(result: FunctionVariantType) {
|
||||
if (!result.isArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rowCount = (result as ArrayValueObject).getRowCount();
|
||||
const colCount = (result as ArrayValueObject).getColumnCount();
|
||||
|
||||
if (rowCount <= 1 && colCount <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._runtimeService.setUnitArrayFormulaEmbeddedMap();
|
||||
}
|
||||
}
|
||||
|
||||
export class OperatorNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@IFormulaRuntimeService private readonly _runtimeService: IFormulaRuntimeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.OPERATOR) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override create(param: string): BaseAstNode {
|
||||
let functionName = '';
|
||||
const tokenTrim = param;
|
||||
if (tokenTrim === operatorToken.PLUS) {
|
||||
functionName = FUNCTION_NAMES_META.PLUS;
|
||||
} else if (tokenTrim === operatorToken.MINUS) {
|
||||
functionName = FUNCTION_NAMES_META.MINUS;
|
||||
} else if (tokenTrim === operatorToken.MULTIPLY) {
|
||||
functionName = FUNCTION_NAMES_META.MULTIPLY;
|
||||
} else if (tokenTrim === operatorToken.DIVIDED) {
|
||||
functionName = FUNCTION_NAMES_META.DIVIDED;
|
||||
} else if (tokenTrim === operatorToken.CONCATENATE) {
|
||||
functionName = FUNCTION_NAMES_TEXT.CONCATENATE;
|
||||
} else if (tokenTrim === operatorToken.POWER) {
|
||||
functionName = FUNCTION_NAMES_MATH.POWER;
|
||||
} else if (OPERATOR_TOKEN_COMPARE_SET.has(tokenTrim)) {
|
||||
functionName = FUNCTION_NAMES_META.COMPARE;
|
||||
}
|
||||
|
||||
const functionExecutor = this._functionService.getExecutor(functionName);
|
||||
if (!functionExecutor) {
|
||||
console.error(`No function ${param}`);
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
return new OperatorNode(tokenTrim, functionExecutor, this._runtimeService);
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (param instanceof LexerNode) {
|
||||
return;
|
||||
}
|
||||
const tokenTrim = param.trim();
|
||||
|
||||
if (tokenTrim.charAt(0) === '"' && tokenTrim.charAt(tokenTrim.length - 1) === '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (OPERATOR_TOKEN_SET.has(tokenTrim)) {
|
||||
return this.create(tokenTrim);
|
||||
}
|
||||
}
|
||||
}
|
||||
193
packages/engine-formula/src/engine/ast-node/prefix-node.ts
Normal file
193
packages/engine-formula/src/engine/ast-node/prefix-node.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { BaseFunction } from '../../functions/base-function';
|
||||
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
|
||||
import type { BaseValueObject } from '../value-object/base-value-object';
|
||||
import { DateSystem } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { OPERATOR_TOKEN_COMPARE_SET, prefixToken } from '../../basics/token';
|
||||
import { FUNCTION_NAMES_LOOKUP } from '../../functions/lookup/function-names';
|
||||
import { FUNCTION_NAMES_META } from '../../functions/meta/function-names';
|
||||
import { IFunctionService } from '../../services/function.service';
|
||||
import { IFormulaRuntimeService } from '../../services/runtime.service';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { ArrayValueObject } from '../value-object/array-value-object';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { NumberValueObject } from '../value-object/primitive-object';
|
||||
import { BaseAstNode, ErrorNode } 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';
|
||||
|
||||
export class PrefixNode extends BaseAstNode {
|
||||
constructor(
|
||||
private _runtimeService: IFormulaRuntimeService,
|
||||
private _operatorString: string,
|
||||
private _functionExecutor?: Nullable<BaseFunction>
|
||||
) {
|
||||
super(_operatorString);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.PREFIX;
|
||||
}
|
||||
|
||||
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
|
||||
const children = this.getChildren();
|
||||
let value = children[0].getValue();
|
||||
let result: FunctionVariantType;
|
||||
if (value == null) {
|
||||
console.error('PrefixNode execute value is null');
|
||||
this.setValue(ErrorValueObject.create(ErrorType.VALUE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._operatorString === prefixToken.MINUS) {
|
||||
if (value.isReferenceObject()) {
|
||||
value = (value as BaseReferenceObject).withDateSystem(dateSystem).toArrayValueObject();
|
||||
}
|
||||
result = this._functionExecutor!.calculate(
|
||||
NumberValueObject.create(0),
|
||||
(value as BaseValueObject).withDateSystem(dateSystem)
|
||||
) as FunctionVariantType;
|
||||
} else if (this._operatorString === prefixToken.AT) {
|
||||
result = this._handlerAT(value);
|
||||
} else {
|
||||
result = ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
this.setValue(result.withDateSystem(dateSystem));
|
||||
}
|
||||
|
||||
private _handlerAT(value: FunctionVariantType) {
|
||||
if (value instanceof ArrayValueObject) {
|
||||
return value.get(0, 0) ?? ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
if (!value.isReferenceObject()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const currentValue = value as BaseReferenceObject;
|
||||
|
||||
if (currentValue.isCell()) {
|
||||
return currentValue.getCellByPosition();
|
||||
}
|
||||
|
||||
if (this._preserveAtRangeForFormula2LookupArray()) {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
const runtimeService = this._runtimeService;
|
||||
|
||||
const currentRow = runtimeService.currentRow || 0;
|
||||
const currentColumn = runtimeService.currentColumn || 0;
|
||||
|
||||
const rangePos = currentValue.getRangePosition();
|
||||
|
||||
const { startRow, startColumn, endRow, endColumn } = rangePos;
|
||||
|
||||
if (endColumn !== startColumn && endRow !== startRow) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
if (startRow === endRow && startColumn === endColumn) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
// @ projection to current
|
||||
if (endRow === startRow && currentColumn >= startColumn && currentColumn <= endColumn) {
|
||||
return currentValue.getCellByColumn(currentColumn);
|
||||
} else if (startColumn === endColumn && currentRow >= startRow && currentRow <= endRow) {
|
||||
return currentValue.getCellByRow(currentRow);
|
||||
}
|
||||
|
||||
if (currentValue.isTable()) {
|
||||
return currentValue.getCellByPosition(currentRow);
|
||||
}
|
||||
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
private _preserveAtRangeForFormula2LookupArray(): boolean {
|
||||
let node: BaseAstNode = this;
|
||||
let parent = node.getParent();
|
||||
let sawCompareOperator = false;
|
||||
|
||||
while (parent != null) {
|
||||
if (parent.nodeType === NodeType.OPERATOR) {
|
||||
sawCompareOperator ||= OPERATOR_TOKEN_COMPARE_SET.has(parent.getToken());
|
||||
}
|
||||
|
||||
if (parent.nodeType === NodeType.FUNCTION) {
|
||||
const functionName = parent.getToken().toUpperCase();
|
||||
if (
|
||||
sawCompareOperator &&
|
||||
functionName === FUNCTION_NAMES_LOOKUP.XMATCH
|
||||
) {
|
||||
return parent.getChildren().indexOf(node) === 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
node = parent;
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrefixNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@IFormulaRuntimeService private readonly _runtimeService: IFormulaRuntimeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.PREFIX) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (!(param instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = param.getToken();
|
||||
const tokenTrim = token.trim();
|
||||
|
||||
if (tokenTrim.charAt(0) === '"' && tokenTrim.charAt(tokenTrim.length - 1) === '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
let functionName = '';
|
||||
if (tokenTrim === prefixToken.MINUS) {
|
||||
functionName = FUNCTION_NAMES_META.MINUS;
|
||||
} else if (tokenTrim === prefixToken.AT) {
|
||||
return new PrefixNode(this._runtimeService, tokenTrim);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const functionExecutor = this._functionService.getExecutor(functionName);
|
||||
if (!functionExecutor) {
|
||||
console.error(`No function ${token}`);
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
return new PrefixNode(this._runtimeService, tokenTrim, functionExecutor);
|
||||
}
|
||||
}
|
||||
385
packages/engine-formula/src/engine/ast-node/reference-node.ts
Normal file
385
packages/engine-formula/src/engine/ast-node/reference-node.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { BaseReferenceObject } from '../reference-object/base-reference-object';
|
||||
import { AstNodePromiseType } from '../../basics/common';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import {
|
||||
regexTestReferenceTableAllColumn,
|
||||
regexTestReferenceTableMultipleColumn,
|
||||
regexTestReferenceTableSingleColumn,
|
||||
regexTestReferenceTableTitleOnlyAnyHash,
|
||||
regexTestSingeRange,
|
||||
regexTestSingleColumn,
|
||||
regexTestSingleRow,
|
||||
} from '../../basics/regex';
|
||||
import { matchToken } from '../../basics/token';
|
||||
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
|
||||
import { IFormulaExternalReferenceDataLoader } from '../../services/external-reference-data-loader.service';
|
||||
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';
|
||||
import { splitTableStructuredRef } from '../utils/reference';
|
||||
import { getReferenceObjectFromCache, ReferenceObjectType } from '../utils/value-object';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
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;
|
||||
}
|
||||
|
||||
type ReferenceNodeCurrentConfigService = Pick<
|
||||
IFormulaCurrentConfigService,
|
||||
'getArrayFormulaCellData' | 'getArrayFormulaRange' | 'getSheetNameMap' | 'getUnitData' | 'getUnitStylesData'
|
||||
>;
|
||||
|
||||
type ReferenceNodeRuntimeService = Pick<
|
||||
IFormulaRuntimeService,
|
||||
| 'currentColumn'
|
||||
| 'currentRow'
|
||||
| 'currentSubUnitId'
|
||||
| 'currentUnitId'
|
||||
| 'getRuntimeArrayFormulaCellData'
|
||||
| 'getRuntimeFeatureCellData'
|
||||
| 'getUnitArrayFormula'
|
||||
| 'getUnitData'
|
||||
>;
|
||||
|
||||
type ReferenceNodeSuperTableService = Pick<
|
||||
ISuperTableService,
|
||||
'getTableMap' | 'getTableOptionMap'
|
||||
>;
|
||||
|
||||
export class ReferenceNode extends BaseAstNode {
|
||||
private _refOffsetX = 0;
|
||||
private _refOffsetY = 0;
|
||||
|
||||
constructor(
|
||||
private _currentConfigService: ReferenceNodeCurrentConfigService,
|
||||
private _runtimeService: ReferenceNodeRuntimeService,
|
||||
operatorString: string,
|
||||
private _referenceObjectType: ReferenceObjectType,
|
||||
private _unitReferenceResolver: IFormulaUnitReferenceResolver,
|
||||
private _superTableService: ReferenceNodeSuperTableService,
|
||||
private _externalReferenceDataLoader: IFormulaExternalReferenceDataLoader,
|
||||
private _isPrepareMerge: boolean = false,
|
||||
private _tableReference?: ITableReferenceDescriptor
|
||||
) {
|
||||
super(operatorString);
|
||||
const unitQualifier = _tableReference
|
||||
? _tableReference.unitQualifier
|
||||
: getReferenceObjectFromCache(operatorString, _referenceObjectType).getUnitQualifier();
|
||||
if (unitQualifier) {
|
||||
this.setAsync();
|
||||
}
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.REFERENCE;
|
||||
}
|
||||
|
||||
override execute() {
|
||||
const currentConfigService = this._currentConfigService;
|
||||
const runtimeService = this._runtimeService;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
this._configureReferenceObject(referenceObject, currentConfigService, runtimeService);
|
||||
|
||||
if (!this._isPrepareMerge && referenceObject.isExceedRange()) {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.NAME));
|
||||
} else {
|
||||
this.setValue(referenceObject);
|
||||
}
|
||||
}
|
||||
|
||||
override async executeAsync(): Promise<AstNodePromiseType> {
|
||||
const hostUnitId = this._runtimeService.currentUnitId;
|
||||
const unitQualifier = this._tableReference
|
||||
? this._tableReference.unitQualifier
|
||||
: getReferenceObjectFromCache(this.getToken(), this._referenceObjectType).getUnitQualifier();
|
||||
if (!unitQualifier) {
|
||||
this.execute();
|
||||
return AstNodePromiseType.SUCCESS;
|
||||
}
|
||||
|
||||
const referenceKind = this._tableReference ? 'table' : 'a1';
|
||||
const resolution = this._unitReferenceResolver.resolve({
|
||||
hostUnitId,
|
||||
qualifier: unitQualifier,
|
||||
referenceKind,
|
||||
});
|
||||
if (typeof resolution === 'string') {
|
||||
this.setValue(ErrorValueObject.create(resolution));
|
||||
return AstNodePromiseType.ERROR;
|
||||
}
|
||||
|
||||
if (resolution.externalReference) {
|
||||
const token = this._getExternalLoadToken();
|
||||
const error = await this._externalReferenceDataLoader.load({
|
||||
hostUnitId,
|
||||
qualifier: unitQualifier,
|
||||
referenceKind,
|
||||
token,
|
||||
tableName: this._tableReference?.tableName,
|
||||
columnStruct: this._tableReference?.columnStruct,
|
||||
resolution,
|
||||
});
|
||||
if (error) {
|
||||
this.setValue(ErrorValueObject.create(error));
|
||||
return AstNodePromiseType.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
this.execute();
|
||||
return AstNodePromiseType.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* A1 ranges are represented as two ReferenceNodes under a `:` UnionNode.
|
||||
* The qualified left node owns the external read, so include the right
|
||||
* boundary and materialize the whole rectangular range in one request.
|
||||
*/
|
||||
private _getExternalLoadToken(): string {
|
||||
const parent = this.getParent();
|
||||
if (parent?.nodeType !== NodeType.UNION || parent.getToken() !== matchToken.COLON) {
|
||||
return this.getToken();
|
||||
}
|
||||
const [left, right] = parent.getChildren();
|
||||
if (left !== this || right == null) {
|
||||
return this.getToken();
|
||||
}
|
||||
return `${this.getToken()}${matchToken.COLON}${right.getToken()}`;
|
||||
}
|
||||
|
||||
private _configureReferenceObject(
|
||||
referenceObject: BaseReferenceObject,
|
||||
currentConfigService: ReferenceNodeCurrentConfigService,
|
||||
runtimeService: ReferenceNodeRuntimeService
|
||||
): 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;
|
||||
}
|
||||
|
||||
getRefOffset() {
|
||||
return {
|
||||
x: this._refOffsetX,
|
||||
y: this._refOffsetY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ReferenceNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService,
|
||||
@IFormulaRuntimeService private readonly _formulaRuntimeService: IFormulaRuntimeService,
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@ISuperTableService private readonly _superTableService: ISuperTableService,
|
||||
@IFormulaUnitReferenceResolver private readonly _unitReferenceResolver: IFormulaUnitReferenceResolver,
|
||||
@IFormulaExternalReferenceDataLoader
|
||||
private readonly _externalReferenceDataLoader: IFormulaExternalReferenceDataLoader
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.REFERENCE) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
let isLexerNode = false;
|
||||
let tokenTrimPre: string;
|
||||
let isPrepareMerge = false;
|
||||
if (param instanceof LexerNode) {
|
||||
isLexerNode = true;
|
||||
tokenTrimPre = param.getToken().trim();
|
||||
|
||||
/**
|
||||
* If this node is a reference to a range,
|
||||
* it is necessary to determine whether it will be combined into a single range by a union operation.
|
||||
*/
|
||||
if (param.getParent()?.getParent()?.getToken().trim() === matchToken.COLON) {
|
||||
isPrepareMerge = true;
|
||||
}
|
||||
} else {
|
||||
tokenTrimPre = param.trim();
|
||||
}
|
||||
|
||||
// const tokenTrim = param.trim();
|
||||
// if (regexTestSingeRange(tokenTrim)) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
const { tokenTrim, minusPrefixNode, atPrefixNode } = prefixHandler(tokenTrimPre, this._functionService, this._formulaRuntimeService);
|
||||
|
||||
if (!isLexerNode && tokenTrim.charAt(0) === '"' && tokenTrim.charAt(tokenTrim.length - 1) === '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
const node: Nullable<ReferenceNode> = this._getNode(tokenTrim, isLexerNode, isPrepareMerge, param);
|
||||
|
||||
if (node) {
|
||||
if (atPrefixNode) {
|
||||
node.setParent(atPrefixNode);
|
||||
} else if (minusPrefixNode) {
|
||||
node.setParent(minusPrefixNode);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
private _getTableMap() {
|
||||
const unitId = this._formulaRuntimeService.currentUnitId;
|
||||
if (!unitId) {
|
||||
return;
|
||||
}
|
||||
return this._superTableService.getTableMap(unitId);
|
||||
}
|
||||
|
||||
private _getNode(
|
||||
tokenTrim: string,
|
||||
isLexerNode: boolean,
|
||||
isPrepareMerge: boolean,
|
||||
param: LexerNode | string
|
||||
) {
|
||||
const currentConfigService = this._currentConfigService;
|
||||
const runtimeService = this._formulaRuntimeService;
|
||||
|
||||
const makeRef = (type: ReferenceObjectType) =>
|
||||
new ReferenceNode(currentConfigService, runtimeService, tokenTrim, type, this._unitReferenceResolver, this._superTableService, this._externalReferenceDataLoader, isPrepareMerge);
|
||||
|
||||
const tableMap = this._getTableMap();
|
||||
const isSuperTableDirect = tableMap?.has(tokenTrim) ?? false;
|
||||
if (isSuperTableDirect) {
|
||||
return this._getTableReferenceNode(tokenTrim, isPrepareMerge, true);
|
||||
}
|
||||
|
||||
const isCellRange = regexTestSingeRange(tokenTrim);
|
||||
if (isCellRange) {
|
||||
return makeRef(ReferenceObjectType.CELL);
|
||||
}
|
||||
const parentIsUnion = isLexerNode && this._checkParentIsUnionOperator(param as LexerNode);
|
||||
const isRowRef = parentIsUnion && regexTestSingleRow(tokenTrim);
|
||||
|
||||
if (isRowRef) {
|
||||
return makeRef(ReferenceObjectType.ROW);
|
||||
}
|
||||
const isColRef = parentIsUnion && regexTestSingleColumn(tokenTrim);
|
||||
|
||||
if (isColRef) {
|
||||
return makeRef(ReferenceObjectType.COLUMN);
|
||||
}
|
||||
|
||||
return this._getTableReferenceNode(tokenTrim, isPrepareMerge, false);
|
||||
}
|
||||
|
||||
private _getTableReferenceNode(tokenTrim: string, isPrepareMerge: boolean, isSuperTableDirectly: boolean = false) {
|
||||
if (!this._checkTokenIsTableReference(tokenTrim) && !isSuperTableDirectly) {
|
||||
return;
|
||||
}
|
||||
const { unitQualifier, tableName, columnStruct } = splitTableStructuredRef(tokenTrim);
|
||||
const tableMap = this._getTableMap();
|
||||
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,
|
||||
this._externalReferenceDataLoader,
|
||||
isPrepareMerge,
|
||||
{ unitQualifier, tableName, columnStruct }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkTokenIsTableReference(token: string): boolean {
|
||||
return regexTestReferenceTableAllColumn(token) || regexTestReferenceTableSingleColumn(token) || regexTestReferenceTableMultipleColumn(token) || regexTestReferenceTableTitleOnlyAnyHash(token);
|
||||
}
|
||||
|
||||
private _checkParentIsUnionOperator(param: LexerNode) {
|
||||
return param.getParent()?.getParent()?.getToken().trim() === matchToken.COLON;
|
||||
}
|
||||
}
|
||||
157
packages/engine-formula/src/engine/ast-node/suffix-node.ts
Normal file
157
packages/engine-formula/src/engine/ast-node/suffix-node.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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 { BaseFunction } from '../../functions/base-function';
|
||||
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
|
||||
import type { CellReferenceObject } from '../reference-object/cell-reference-object';
|
||||
import type { BaseValueObject } from '../value-object/base-value-object';
|
||||
import { DateSystem, Inject } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { suffixToken } from '../../basics/token';
|
||||
import { FUNCTION_NAMES_META } from '../../functions/meta/function-names';
|
||||
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
|
||||
import { IFunctionService } from '../../services/function.service';
|
||||
import { Lexer } from '../analysis/lexer';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { NumberValueObject } from '../value-object/primitive-object';
|
||||
import { BaseAstNode, ErrorNode } 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';
|
||||
|
||||
export class SuffixNode extends BaseAstNode {
|
||||
constructor(
|
||||
private _currentConfigService: IFormulaCurrentConfigService,
|
||||
private _lexer: Lexer,
|
||||
private _operatorString: string,
|
||||
private _functionExecutor?: BaseFunction
|
||||
) {
|
||||
super(_operatorString);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.SUFFIX;
|
||||
}
|
||||
|
||||
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
|
||||
const children = this.getChildren();
|
||||
let value = children[0]?.getValue();
|
||||
let result: FunctionVariantType;
|
||||
if (value == null) {
|
||||
this.setValue(ErrorValueObject.create(ErrorType.ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._operatorString === suffixToken.PERCENTAGE) {
|
||||
if (value.isReferenceObject()) {
|
||||
value = (value as BaseReferenceObject).withDateSystem(dateSystem).toArrayValueObject();
|
||||
}
|
||||
result = this._functionExecutor!.calculate(
|
||||
(value as BaseValueObject).withDateSystem(dateSystem),
|
||||
NumberValueObject.create(100)
|
||||
) as FunctionVariantType;
|
||||
|
||||
// set number format
|
||||
if ((result as NumberValueObject).isNumber()) {
|
||||
const value = Number((result as NumberValueObject).getValue());
|
||||
result = NumberValueObject.create(value, '0.00%');
|
||||
}
|
||||
} else if (this._operatorString === suffixToken.POUND) {
|
||||
result = this._handlerPound(value);
|
||||
} else {
|
||||
result = ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
this.setValue(result.withDateSystem(dateSystem));
|
||||
}
|
||||
|
||||
private _handlerPound(value: FunctionVariantType) {
|
||||
// const sheetData = interpreterDatasetConfig.sheetData;
|
||||
// if (!sheetData) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (!value.isReferenceObject()) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
if (!(value as BaseReferenceObject).isCell()) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
// const currentConfigService = this._accessor.get(IFormulaCurrentConfigService);
|
||||
|
||||
// const lexer = this._accessor.get(Lexer);
|
||||
|
||||
const cellValue = value as CellReferenceObject;
|
||||
const range = cellValue.getRangePosition();
|
||||
const unitId = cellValue.getUnitId();
|
||||
const sheetId = cellValue.getSheetId();
|
||||
const formulaData = this._currentConfigService.getFormulaData();
|
||||
|
||||
const formulaString = formulaData?.[unitId]?.[sheetId]?.[range.startRow]?.[range.startColumn]?.f;
|
||||
|
||||
if (!formulaString) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
const lexerNode = this._lexer.treeBuilder(formulaString, true, unitId);
|
||||
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
/** todo */
|
||||
}
|
||||
}
|
||||
|
||||
export class SuffixNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@Inject(Lexer) private readonly _lexer: Lexer,
|
||||
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.SUFFIX) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (!(param instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenTrim = param.getToken().trim();
|
||||
|
||||
if (tokenTrim.charAt(0) === '"' && tokenTrim.charAt(tokenTrim.length - 1) === '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
let functionName = '';
|
||||
if (tokenTrim === suffixToken.PERCENTAGE) {
|
||||
functionName = FUNCTION_NAMES_META.DIVIDED;
|
||||
} else if (tokenTrim === suffixToken.POUND) {
|
||||
return new SuffixNode(this._currentConfigService, this._lexer, tokenTrim);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const functionExecutor = this._functionService.getExecutor(functionName);
|
||||
if (!functionExecutor) {
|
||||
console.error(`No function ${param}`);
|
||||
return ErrorNode.create(ErrorType.NAME);
|
||||
}
|
||||
return new SuffixNode(this._currentConfigService, this._lexer, tokenTrim, functionExecutor);
|
||||
}
|
||||
}
|
||||
209
packages/engine-formula/src/engine/ast-node/union-node.ts
Normal file
209
packages/engine-formula/src/engine/ast-node/union-node.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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 { FunctionVariantType } from '../reference-object/base-reference-object';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { matchToken } from '../../basics/token';
|
||||
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
|
||||
import { IFunctionService } from '../../services/function.service';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { BaseReferenceObject } from '../reference-object/base-reference-object';
|
||||
import { MultiAreaArrayMode, MultiAreaReferenceObject } from '../reference-object/multi-area-reference-object';
|
||||
import { RangeReferenceObject } from '../reference-object/range-reference-object';
|
||||
import { getRangeReferenceObjectFromCache } from '../utils/value-object';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
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';
|
||||
|
||||
// const UNION_EXECUTOR_NAME = 'UNION';
|
||||
|
||||
export class UnionNode extends BaseAstNode {
|
||||
constructor(
|
||||
operatorString: string,
|
||||
private readonly _currentConfigService: Pick<IFormulaCurrentConfigService, 'getSheetsInfo'>
|
||||
) {
|
||||
super(operatorString);
|
||||
}
|
||||
|
||||
override get nodeType() {
|
||||
return NodeType.UNION;
|
||||
}
|
||||
|
||||
override execute() {
|
||||
const children = this.getChildren();
|
||||
const leftChild = children[0];
|
||||
const rightChild = children[1];
|
||||
const leftNode = leftChild.getValue();
|
||||
const rightNode = rightChild.getValue();
|
||||
|
||||
if (leftNode == null || rightNode == null) {
|
||||
console.error('UnionNode execute leftNode or rightNode is null');
|
||||
this.setValue(ErrorValueObject.create(ErrorType.VALUE));
|
||||
return;
|
||||
}
|
||||
|
||||
let result: FunctionVariantType;
|
||||
if (this.getToken() === matchToken.COLON) {
|
||||
result = this._createThreeDimensionalReference(leftChild.getToken(), rightNode)
|
||||
?? this._unionFunction(leftNode, rightNode);
|
||||
} else {
|
||||
result = ErrorValueObject.create(ErrorType.NAME);
|
||||
}
|
||||
this.setValue(result);
|
||||
}
|
||||
|
||||
private _createThreeDimensionalReference(
|
||||
firstSheetToken: string,
|
||||
rightNode: FunctionVariantType
|
||||
): MultiAreaReferenceObject | undefined {
|
||||
if (!(rightNode instanceof BaseReferenceObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = rightNode;
|
||||
const firstSheetName = this._normalizeSheetName(firstSheetToken);
|
||||
const lastSheetName = this._normalizeSheetName(source.getForcedSheetName());
|
||||
if (!firstSheetName || !lastSheetName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sheetOrder, sheetNameMap } = this._currentConfigService.getSheetsInfo();
|
||||
const sheetIdsByName = new Map(
|
||||
Object.entries(sheetNameMap).map(([sheetId, sheetName]) => [sheetName.toLocaleLowerCase(), sheetId])
|
||||
);
|
||||
const firstSheetId = sheetIdsByName.get(firstSheetName.toLocaleLowerCase());
|
||||
const lastSheetId = sheetIdsByName.get(lastSheetName.toLocaleLowerCase());
|
||||
const firstIndex = firstSheetId == null ? -1 : sheetOrder.indexOf(firstSheetId);
|
||||
const lastIndex = lastSheetId == null ? -1 : sheetOrder.indexOf(lastSheetId);
|
||||
if (firstIndex < 0 || lastIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = Math.min(firstIndex, lastIndex);
|
||||
const endIndex = Math.max(firstIndex, lastIndex);
|
||||
const areas = sheetOrder.slice(startIndex, endIndex + 1).map((sheetId) => {
|
||||
const reference = new RangeReferenceObject(source.getRangeData(), sheetId, source.getUnitId());
|
||||
this._copyReferenceContext(source, reference);
|
||||
reference.setForcedSheetIdDirect(sheetId);
|
||||
reference.setForcedSheetName(sheetNameMap[sheetId] ?? '');
|
||||
return [reference];
|
||||
});
|
||||
|
||||
return new MultiAreaReferenceObject(
|
||||
`${firstSheetToken}:${source.getToken()}`,
|
||||
areas,
|
||||
MultiAreaArrayMode.STACK_AREAS
|
||||
);
|
||||
}
|
||||
|
||||
private _normalizeSheetName(token: string): string {
|
||||
const trimmed = token.trim();
|
||||
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
||||
return trimmed.slice(1, -1).replace(/''/g, "'");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private _copyReferenceContext(source: BaseReferenceObject, target: BaseReferenceObject): void {
|
||||
target.setDefaultUnitId(source.getDefaultUnitId());
|
||||
target.setDefaultSheetId(source.getDefaultSheetId());
|
||||
target.setUnitData(source.getUnitData());
|
||||
target.setUnitStylesData(source.getUnitStylesData());
|
||||
target.setFilteredOutRows(source.getFilteredOutRows());
|
||||
target.setRuntimeData(source.getRuntimeData());
|
||||
target.setArrayFormulaCellData(source.getArrayFormulaCellData());
|
||||
target.setArrayFormulaRange(source.getArrayFormulaRange());
|
||||
target.setRuntimeArrayFormulaCellData(source.getRuntimeArrayFormulaCellData());
|
||||
target.setRuntimeArrayFormulaRange(source.getRuntimeArrayFormulaRange());
|
||||
target.setRuntimeFeatureCellData(source.getRuntimeFeatureCellData());
|
||||
|
||||
const currentRow = source.getCurrentRow();
|
||||
const currentColumn = source.getCurrentColumn();
|
||||
if (currentRow != null && currentColumn != null) {
|
||||
target.setCurrentRowAndColumn(currentRow, currentColumn);
|
||||
}
|
||||
|
||||
const { x, y } = source.getRefOffset();
|
||||
target.setRefOffset(x, y);
|
||||
}
|
||||
|
||||
private _unionFunction(
|
||||
variant1: FunctionVariantType,
|
||||
variant2: FunctionVariantType
|
||||
): FunctionVariantType {
|
||||
if (variant1.isError() || variant2.isError()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
if (!(variant1 instanceof BaseReferenceObject) || !(variant2 instanceof BaseReferenceObject)) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
return getRangeReferenceObjectFromCache(variant1, variant2);
|
||||
|
||||
// if (variant1.isCell() && variant2.isCell()) {
|
||||
|
||||
// return variant1.unionBy(variant2);
|
||||
// }
|
||||
// if (variant1.isRow() && variant2.isRow()) {
|
||||
// return variant1.unionBy(variant2);
|
||||
// }
|
||||
// if (variant1.isColumn() && variant2.isColumn()) {
|
||||
// return variant1.unionBy(variant2);
|
||||
// }
|
||||
|
||||
// // =A1:A gets #NAME?
|
||||
// return ErrorValueObject.create(ErrorType.NAME);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnionNodeFactory extends BaseAstNodeFactory {
|
||||
constructor(
|
||||
@IFunctionService private readonly _functionService: IFunctionService,
|
||||
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get zIndex() {
|
||||
return NODE_ORDER_MAP.get(NodeType.UNION) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
override create(param: string): BaseAstNode {
|
||||
return new UnionNode(param, this._currentConfigService);
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string) {
|
||||
if (!(param instanceof LexerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = param.getToken();
|
||||
|
||||
const tokenTrim = token.trim();
|
||||
|
||||
if (tokenTrim.charAt(0) === '"' && tokenTrim.charAt(tokenTrim.length - 1) === '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenTrim !== matchToken.COLON) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.create(tokenTrim);
|
||||
}
|
||||
}
|
||||
98
packages/engine-formula/src/engine/ast-node/value-node.ts
Normal file
98
packages/engine-formula/src/engine/ast-node/value-node.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 { ErrorType } from '../../basics/error-type';
|
||||
import type { FunctionNode } from './';
|
||||
import { DateSystem } from '@univerjs/core';
|
||||
import { BooleanValue } from '../../basics/common';
|
||||
import { ERROR_TYPE_SET } from '../../basics/error-type';
|
||||
import { LexerNode } from '../analysis/lexer-node';
|
||||
import { ValueObjectFactory } from '../value-object/array-value-object';
|
||||
import { StringValueObject } from '../value-object/primitive-object';
|
||||
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';
|
||||
|
||||
export class ValueNode extends BaseAstNode {
|
||||
constructor(operatorString: string) {
|
||||
super(operatorString);
|
||||
}
|
||||
|
||||
override get nodeType(): NodeType {
|
||||
return NodeType.VALUE;
|
||||
}
|
||||
|
||||
override execute(dateSystem: DateSystem = DateSystem.Date1900): void {
|
||||
const token = this.getToken();
|
||||
const tokenTrim = token.trim();
|
||||
if (tokenTrim.startsWith('"') && tokenTrim.endsWith('"')) {
|
||||
this.setValue(
|
||||
StringValueObject.create(tokenTrim.slice(1, -1).replace(/""/g, '"')).withDateSystem(dateSystem)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = this.getParent();
|
||||
let isIgnoreNumberPattern = true;
|
||||
if (parent?.nodeType === NodeType.FUNCTION) {
|
||||
isIgnoreNumberPattern = (parent as FunctionNode).isFunctionExecutorArgumentsIgnoreNumberPattern?.() ?? true;
|
||||
}
|
||||
this.setValue(ValueObjectFactory.create(token, isIgnoreNumberPattern, dateSystem));
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueNodeFactory extends BaseAstNodeFactory {
|
||||
override get zIndex(): number {
|
||||
return NODE_ORDER_MAP.get(NodeType.VALUE) || DEFAULT_AST_NODE_FACTORY_Z_INDEX;
|
||||
}
|
||||
|
||||
_checkValueNode(token: string): BaseAstNode | undefined {
|
||||
if (Number.isNaN(Number(token))) {
|
||||
const tokenTrim = token.trim();
|
||||
const startToken = tokenTrim.charAt(0);
|
||||
const endToken = tokenTrim.charAt(tokenTrim.length - 1);
|
||||
|
||||
if (ERROR_TYPE_SET.has(tokenTrim as ErrorType)) {
|
||||
return this.create(tokenTrim);
|
||||
}
|
||||
|
||||
if (startToken === '"' && endToken === '"') {
|
||||
return this.create(tokenTrim);
|
||||
}
|
||||
if (startToken === '{' && endToken === '}') {
|
||||
return this.create(tokenTrim);
|
||||
}
|
||||
|
||||
const tokenTrimUpper = tokenTrim.toUpperCase();
|
||||
if (tokenTrimUpper === BooleanValue.TRUE || tokenTrimUpper === BooleanValue.FALSE) {
|
||||
return this.create(tokenTrimUpper);
|
||||
}
|
||||
} else {
|
||||
return this.create(token);
|
||||
}
|
||||
}
|
||||
|
||||
override create(param: string): BaseAstNode {
|
||||
return new ValueNode(param);
|
||||
}
|
||||
|
||||
override checkAndCreateNodeType(param: LexerNode | string): BaseAstNode | undefined {
|
||||
if (param instanceof LexerNode) {
|
||||
return;
|
||||
}
|
||||
return this._checkValueNode(param);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 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 { IUnitExcludedCell } from '../../../basics/common';
|
||||
import { ObjectMatrix } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
FormulaDependencyTree,
|
||||
FormulaDependencyTreeModel,
|
||||
FormulaDependencyTreeType,
|
||||
FormulaDependencyTreeVirtual,
|
||||
} from '../dependency-tree';
|
||||
|
||||
function createTree(treeId: number) {
|
||||
const tree = new FormulaDependencyTree(treeId);
|
||||
tree.unitId = 'unit-1';
|
||||
tree.subUnitId = 'sheet-1';
|
||||
tree.formula = '=A1+B1';
|
||||
tree.row = 2;
|
||||
tree.column = 3;
|
||||
tree.rowCount = 20;
|
||||
tree.columnCount = 10;
|
||||
tree.rangeList = [
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
range: {
|
||||
startRow: 1,
|
||||
startColumn: 1,
|
||||
endRow: 2,
|
||||
endColumn: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
return tree;
|
||||
}
|
||||
|
||||
describe('FormulaDependencyTree', () => {
|
||||
it('should track tree state and parent child relations', () => {
|
||||
const parent = createTree(1);
|
||||
const child = createTree(2);
|
||||
|
||||
parent.setAdded();
|
||||
expect(parent.isAdded()).toBe(true);
|
||||
parent.resetState();
|
||||
expect(parent.isAdded()).toBe(false);
|
||||
|
||||
parent.setSkip();
|
||||
expect(parent.isSkip()).toBe(true);
|
||||
|
||||
parent.pushChildren(child);
|
||||
expect(parent.hasChildren(2)).toBe(true);
|
||||
expect(child.parents.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle basic range and dependency checks', () => {
|
||||
const tree = createTree(10);
|
||||
|
||||
expect(tree.inRangeData({ startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 })).toBe(true);
|
||||
expect(tree.inRangeData({ startRow: 5, startColumn: 5, endRow: 6, endColumn: 6 })).toBe(false);
|
||||
|
||||
expect(tree.dependencySheetName()).toBe(false);
|
||||
expect(tree.dependencySheetName({ 'unit-1': { 'sheet-1': 'Main' } })).toBe(true);
|
||||
|
||||
tree.pushRangeList([
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
range: { startRow: 8, startColumn: 8, endRow: 8, endColumn: 8 },
|
||||
},
|
||||
]);
|
||||
expect(tree.rangeList.length).toBe(2);
|
||||
expect(tree.shouldBePushRangeList()).toBe(false);
|
||||
});
|
||||
|
||||
it('should use feature ranges when tree is feature formula', () => {
|
||||
const tree = createTree(12);
|
||||
tree.featureId = 'feature-1';
|
||||
tree.featureDirtyRanges = [
|
||||
{
|
||||
unitId: 'u',
|
||||
sheetId: 's',
|
||||
range: { startRow: 9, startColumn: 9, endRow: 10, endColumn: 10 },
|
||||
},
|
||||
];
|
||||
tree.type = FormulaDependencyTreeType.FEATURE_FORMULA;
|
||||
tree.rangeList = [];
|
||||
|
||||
expect(tree.shouldBePushRangeList()).toBe(false);
|
||||
expect(tree.toRTreeItem()).toEqual(tree.featureDirtyRanges);
|
||||
});
|
||||
|
||||
it('should detect excluded range with finite and NaN boundaries', () => {
|
||||
const tree = createTree(13);
|
||||
tree.rangeList = [
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
range: { startRow: Number.NaN, startColumn: Number.NaN, endRow: Number.NaN, endColumn: Number.NaN },
|
||||
},
|
||||
];
|
||||
|
||||
const excluded: IUnitExcludedCell = {
|
||||
'unit-1': {
|
||||
'sheet-1': new ObjectMatrix<boolean>({
|
||||
0: { 0: true },
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
expect(tree.isExcludeRange(excluded)).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear internals on dispose', () => {
|
||||
const tree = createTree(14);
|
||||
tree.featureDirtyRanges = [
|
||||
{
|
||||
unitId: 'u',
|
||||
sheetId: 's',
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
|
||||
},
|
||||
];
|
||||
tree.getDirtyData = (() => ({
|
||||
runtimeCellData: {},
|
||||
dirtyRanges: {},
|
||||
})) as never;
|
||||
|
||||
tree.dispose();
|
||||
|
||||
expect(tree.featureDirtyRanges).toEqual([]);
|
||||
expect(tree.rangeList).toEqual([]);
|
||||
expect(tree.getDirtyData).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormulaDependencyTreeVirtual', () => {
|
||||
it('should expose defaults when no ref tree exists', () => {
|
||||
const tree = new FormulaDependencyTreeVirtual();
|
||||
|
||||
expect(tree.row).toBe(-1);
|
||||
expect(tree.column).toBe(-1);
|
||||
expect(tree.rowCount).toBe(0);
|
||||
expect(tree.columnCount).toBe(0);
|
||||
expect(tree.unitId).toBe('');
|
||||
expect(tree.subUnitId).toBe('');
|
||||
expect(tree.formula).toBe('');
|
||||
expect(tree.formulaId).toBe('');
|
||||
expect(tree.rangeList).toEqual([]);
|
||||
expect(tree.toRTreeItem()[0]?.range.startRow).toBe(-1);
|
||||
});
|
||||
|
||||
it('should proxy properties and offset ranges from ref tree', () => {
|
||||
const refTree = createTree(30);
|
||||
refTree.formulaId = 'formula-id-1';
|
||||
const virtualTree = new FormulaDependencyTreeVirtual();
|
||||
virtualTree.treeId = 31;
|
||||
virtualTree.refTree = refTree;
|
||||
virtualTree.refOffsetX = 2;
|
||||
virtualTree.refOffsetY = 3;
|
||||
|
||||
expect(virtualTree.row).toBe(5);
|
||||
expect(virtualTree.column).toBe(5);
|
||||
expect(virtualTree.rowCount).toBe(refTree.rowCount);
|
||||
expect(virtualTree.columnCount).toBe(refTree.columnCount);
|
||||
expect(virtualTree.unitId).toBe('unit-1');
|
||||
expect(virtualTree.subUnitId).toBe('sheet-1');
|
||||
expect(virtualTree.formula).toBe('=A1+B1');
|
||||
expect(virtualTree.formulaId).toBe('formula-id-1');
|
||||
expect(virtualTree.nodeData.refOffsetX).toBe(2);
|
||||
expect(virtualTree.nodeData.refOffsetY).toBe(3);
|
||||
expect(virtualTree.rangeList[0]?.range).toEqual({
|
||||
startRow: 4,
|
||||
startColumn: 3,
|
||||
endRow: 5,
|
||||
endColumn: 4,
|
||||
});
|
||||
|
||||
expect(virtualTree.inRangeData({ startRow: 5, startColumn: 5, endRow: 6, endColumn: 6 })).toBe(true);
|
||||
expect(virtualTree.dependencySheetName({ 'unit-1': { 'sheet-1': 'Main' } })).toBe(true);
|
||||
|
||||
virtualTree.dispose();
|
||||
expect(virtualTree.refTree).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormulaDependencyTreeModel', () => {
|
||||
it('should serialize normal and virtual trees', () => {
|
||||
const tree = createTree(100);
|
||||
tree.formulaId = 'fid';
|
||||
tree.featureId = 'feature-1';
|
||||
tree.type = FormulaDependencyTreeType.NORMAL_FORMULA;
|
||||
|
||||
const childTree = createTree(101);
|
||||
|
||||
const model = new FormulaDependencyTreeModel(tree);
|
||||
const childModel = new FormulaDependencyTreeModel(childTree);
|
||||
model.addChild(childModel);
|
||||
childModel.addParent(model);
|
||||
|
||||
const json = model.toJson();
|
||||
expect(json.treeId).toBe(100);
|
||||
expect(json.children).toEqual([101]);
|
||||
expect(json.parents).toEqual([]);
|
||||
expect(json.formulaId).toBe('fid');
|
||||
expect(json.featureId).toBe('feature-1');
|
||||
|
||||
const fullJson = childModel.toFullJson();
|
||||
expect(fullJson.parents[0]?.treeId).toBe(100);
|
||||
|
||||
const virtual = new FormulaDependencyTreeVirtual();
|
||||
virtual.treeId = 200;
|
||||
virtual.refTree = tree;
|
||||
const virtualModel = new FormulaDependencyTreeModel(virtual);
|
||||
expect(virtualModel.refTreeId).toBe(100);
|
||||
expect(virtualModel.formula).toBe('');
|
||||
});
|
||||
});
|
||||
644
packages/engine-formula/src/engine/dependency/dependency-tree.ts
Normal file
644
packages/engine-formula/src/engine/dependency/dependency-tree.ts
Normal file
@@ -0,0 +1,644 @@
|
||||
/**
|
||||
* 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 { IRange, IUnitRange, Nullable } from '@univerjs/core';
|
||||
|
||||
import type {
|
||||
IDirtyUnitSheetNameMap,
|
||||
IFeatureDirtyRangeType,
|
||||
IRuntimeUnitDataType,
|
||||
IUnitExcludedCell,
|
||||
} from '../../basics/common';
|
||||
import type { IFormulaDirtyData } from '../../services/current-data.service';
|
||||
import type { IAllRuntimeData } from '../../services/runtime.service';
|
||||
import type { AstRootNode, FunctionNode } from '../ast-node';
|
||||
import { moveRangeByOffset } from '@univerjs/core';
|
||||
|
||||
export enum FDtreeStateType {
|
||||
DEFAULT,
|
||||
ADDED,
|
||||
SKIP,
|
||||
}
|
||||
|
||||
export enum FormulaDependencyTreeType {
|
||||
NORMAL_FORMULA,
|
||||
OTHER_FORMULA,
|
||||
FEATURE_FORMULA,
|
||||
}
|
||||
|
||||
// export interface IUnitRangeWithToken {
|
||||
// gridRange: IUnitRange;
|
||||
// token: string;
|
||||
// }
|
||||
|
||||
class FormulaDependencyTreeCalculator {
|
||||
private _state = FDtreeStateType.DEFAULT;
|
||||
|
||||
type: FormulaDependencyTreeType = FormulaDependencyTreeType.NORMAL_FORMULA;
|
||||
|
||||
resetState() {
|
||||
this._state = FDtreeStateType.DEFAULT;
|
||||
}
|
||||
|
||||
setAdded() {
|
||||
this._state = FDtreeStateType.ADDED;
|
||||
}
|
||||
|
||||
isAdded() {
|
||||
return this._state === FDtreeStateType.ADDED;
|
||||
}
|
||||
|
||||
setSkip() {
|
||||
this._state = FDtreeStateType.SKIP;
|
||||
}
|
||||
|
||||
isSkip() {
|
||||
return this._state === FDtreeStateType.SKIP;
|
||||
}
|
||||
|
||||
treeId: number;
|
||||
|
||||
children: Set<number> = new Set();
|
||||
|
||||
parents: Set<number> = new Set();
|
||||
|
||||
pushChildren(tree: FormulaDependencyTreeCalculator) {
|
||||
this.children.add(tree.treeId);
|
||||
tree._pushParent(this);
|
||||
}
|
||||
|
||||
hasChildren(treeId: number) {
|
||||
return this.children.has(treeId);
|
||||
}
|
||||
|
||||
private _pushParent(tree: FormulaDependencyTreeCalculator) {
|
||||
this.parents.add(tree.treeId);
|
||||
}
|
||||
}
|
||||
|
||||
type GetDirtyDataType = Nullable<
|
||||
(dirtyData: IFormulaDirtyData, runtimeData: IAllRuntimeData) => {
|
||||
runtimeCellData: IRuntimeUnitDataType;
|
||||
dirtyRanges: IFeatureDirtyRangeType;
|
||||
}
|
||||
>;
|
||||
export type IFormulaDependencyTree = FormulaDependencyTree | FormulaDependencyTreeVirtual;
|
||||
|
||||
export class FormulaDependencyTreeVirtual extends FormulaDependencyTreeCalculator {
|
||||
refTree: Nullable<FormulaDependencyTree>;
|
||||
refOffsetX: number = -1;
|
||||
refOffsetY: number = -1;
|
||||
isCache: boolean = false;
|
||||
isDirty: boolean = false;
|
||||
|
||||
addressFunctionNodes: FunctionNode[] = [];
|
||||
|
||||
get isVirtual() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get row() {
|
||||
if (this.refTree == null) {
|
||||
return -1;
|
||||
}
|
||||
return this.refTree.row + this.refOffsetY;
|
||||
}
|
||||
|
||||
get column() {
|
||||
if (this.refTree == null) {
|
||||
return -1;
|
||||
}
|
||||
return this.refTree.column + this.refOffsetX;
|
||||
}
|
||||
|
||||
get rowCount() {
|
||||
if (this.refTree == null) {
|
||||
return 0;
|
||||
}
|
||||
return this.refTree.rowCount;
|
||||
}
|
||||
|
||||
get columnCount() {
|
||||
if (this.refTree == null) {
|
||||
return 0;
|
||||
}
|
||||
return this.refTree.columnCount;
|
||||
}
|
||||
|
||||
get unitId() {
|
||||
if (this.refTree == null) {
|
||||
return '';
|
||||
}
|
||||
return this.refTree.unitId;
|
||||
}
|
||||
|
||||
get subUnitId() {
|
||||
if (this.refTree == null) {
|
||||
return '';
|
||||
}
|
||||
return this.refTree.subUnitId;
|
||||
}
|
||||
|
||||
get formula() {
|
||||
return this.refTree?.formula ?? '';
|
||||
}
|
||||
|
||||
get nodeData() {
|
||||
return {
|
||||
node: this.node,
|
||||
refOffsetX: this.refOffsetX,
|
||||
refOffsetY: this.refOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
get node() {
|
||||
return this.refTree?.node;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.refTree = null;
|
||||
}
|
||||
|
||||
get rangeList() {
|
||||
const unitRangeList = [];
|
||||
if (this.refTree == null) {
|
||||
return [];
|
||||
}
|
||||
for (let i = 0; i < this.refTree.rangeList.length; i++) {
|
||||
const range = this.refTree.rangeList[i];
|
||||
unitRangeList.push({
|
||||
unitId: range.unitId,
|
||||
sheetId: range.sheetId,
|
||||
range: moveRangeByOffset(range.range, this.refOffsetX, this.refOffsetY),
|
||||
});
|
||||
}
|
||||
return unitRangeList;
|
||||
}
|
||||
|
||||
toRTreeItem(): IUnitRange[] {
|
||||
const currentRow = this.row;
|
||||
const currentColumn = this.column;
|
||||
|
||||
return [{
|
||||
unitId: this.unitId,
|
||||
sheetId: this.subUnitId,
|
||||
range: {
|
||||
startRow: currentRow,
|
||||
startColumn: currentColumn,
|
||||
endRow: currentRow,
|
||||
endColumn: currentColumn,
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
inRangeData(range: IRange) {
|
||||
const startRow = range.startRow;
|
||||
const startColumn = range.startColumn;
|
||||
const endRow = range.endRow;
|
||||
const endColumn = range.endColumn;
|
||||
|
||||
const currentRow = this.row;
|
||||
const currentColumn = this.column;
|
||||
|
||||
if (currentRow < startRow || currentRow > endRow || currentColumn < startColumn || currentColumn > endColumn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
dependencySheetName(dirtyUnitSheetNameMap?: IDirtyUnitSheetNameMap) {
|
||||
if (this.refTree == null) {
|
||||
return false;
|
||||
}
|
||||
return this.refTree.dependencySheetName(dirtyUnitSheetNameMap);
|
||||
}
|
||||
|
||||
isExcludeRange(unitExcludedCell: Nullable<IUnitExcludedCell>) {
|
||||
const rangeList = this.rangeList;
|
||||
|
||||
if (rangeList.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let r = 0, len = rangeList.length; r < len; r++) {
|
||||
const unitRange = rangeList[r];
|
||||
const { unitId, sheetId, range } = unitRange;
|
||||
|
||||
const excludedCell = unitExcludedCell?.[unitId]?.[sheetId];
|
||||
let { startRow: rangeStartRow, endRow: rangeEndRow, startColumn: rangeStartColumn, endColumn: rangeEndColumn } = range;
|
||||
|
||||
if (Number.isNaN(rangeStartRow)) {
|
||||
rangeStartRow = 0;
|
||||
}
|
||||
if (Number.isNaN(rangeStartColumn)) {
|
||||
rangeStartColumn = 0;
|
||||
}
|
||||
if (Number.isNaN(rangeEndRow)) {
|
||||
rangeEndRow = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
if (Number.isNaN(rangeEndColumn)) {
|
||||
rangeEndColumn = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
let isInclude = false;
|
||||
|
||||
excludedCell?.forValue((row, column) => {
|
||||
if (row >= rangeStartRow && row <= rangeEndRow && column >= rangeStartColumn && column <= rangeEndColumn) {
|
||||
isInclude = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isInclude) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getDirtyData: GetDirtyDataType;
|
||||
|
||||
featureId: Nullable<string>;
|
||||
get formulaId() {
|
||||
if (this.refTree == null) {
|
||||
return '';
|
||||
}
|
||||
return this.refTree.formulaId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dependency tree, capable of calculating mutual dependencies,
|
||||
* is used to determine the order of formula calculations.
|
||||
*/
|
||||
export class FormulaDependencyTree extends FormulaDependencyTreeCalculator {
|
||||
isCache: boolean = false;
|
||||
|
||||
featureId: Nullable<string>;
|
||||
featureDirtyRanges: IUnitRange[] = [];
|
||||
|
||||
refOffsetX: number = 0;
|
||||
refOffsetY: number = 0;
|
||||
|
||||
formulaId: Nullable<string>;
|
||||
|
||||
subUnitId: string = '';
|
||||
|
||||
unitId: string = '';
|
||||
|
||||
rangeList: IUnitRange[] = [];
|
||||
|
||||
formula: string = '';
|
||||
|
||||
row: number = -1;
|
||||
|
||||
column: number = -1;
|
||||
|
||||
rowCount: number = Number.NEGATIVE_INFINITY;
|
||||
|
||||
columnCount: number = Number.NEGATIVE_INFINITY;
|
||||
|
||||
isDirty: boolean = false;
|
||||
|
||||
node: Nullable<AstRootNode>;
|
||||
|
||||
addressFunctionNodes: FunctionNode[] = [];
|
||||
|
||||
constructor(treeId: number) {
|
||||
super();
|
||||
this.treeId = treeId;
|
||||
}
|
||||
|
||||
get isVirtual() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get nodeData() {
|
||||
return {
|
||||
node: this.node,
|
||||
refOffsetX: 0,
|
||||
refOffsetY: 0,
|
||||
};
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
formula: this.formula,
|
||||
refOffsetX: this.refOffsetX,
|
||||
refOffsetY: this.refOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
getDirtyData: GetDirtyDataType;
|
||||
|
||||
dispose(): void {
|
||||
this.featureDirtyRanges = [];
|
||||
|
||||
this.rangeList = [];
|
||||
|
||||
this.addressFunctionNodes = [];
|
||||
|
||||
// this.nodeData?.node.dispose();
|
||||
|
||||
this.getDirtyData = null;
|
||||
}
|
||||
|
||||
inRangeData(range: IRange) {
|
||||
const startRow = range.startRow;
|
||||
const startColumn = range.startColumn;
|
||||
const endRow = range.endRow;
|
||||
const endColumn = range.endColumn;
|
||||
|
||||
const currentRow = this.row;
|
||||
const currentColumn = this.column;
|
||||
|
||||
if (currentRow < startRow || currentRow > endRow || currentColumn < startColumn || currentColumn > endColumn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
dependencySheetName(dirtyUnitSheetNameMap?: IDirtyUnitSheetNameMap) {
|
||||
const rangeList = this.rangeList;
|
||||
|
||||
if (rangeList.length === 0 || dirtyUnitSheetNameMap == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let r = 0, len = rangeList.length; r < len; r++) {
|
||||
const unitRange = rangeList[r];
|
||||
const { unitId, sheetId } = unitRange;
|
||||
|
||||
/**
|
||||
* When a worksheet is inserted or deleted,
|
||||
* the formulas that depend on these worksheets need to be calculated.
|
||||
*/
|
||||
if (dirtyUnitSheetNameMap[unitId]?.[sheetId] != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isExcludeRange(unitExcludedCell: Nullable<IUnitExcludedCell>) {
|
||||
const rangeList = this.rangeList;
|
||||
|
||||
if (rangeList.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let r = 0, len = rangeList.length; r < len; r++) {
|
||||
const unitRange = rangeList[r];
|
||||
const { unitId, sheetId, range } = unitRange;
|
||||
|
||||
const excludedCell = unitExcludedCell?.[unitId]?.[sheetId];
|
||||
let { startRow: rangeStartRow, endRow: rangeEndRow, startColumn: rangeStartColumn, endColumn: rangeEndColumn } = range;
|
||||
|
||||
if (Number.isNaN(rangeStartRow)) {
|
||||
rangeStartRow = 0;
|
||||
}
|
||||
if (Number.isNaN(rangeStartColumn)) {
|
||||
rangeStartColumn = 0;
|
||||
}
|
||||
if (Number.isNaN(rangeEndRow)) {
|
||||
rangeEndRow = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
if (Number.isNaN(rangeEndColumn)) {
|
||||
rangeEndColumn = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
let isInclude = false;
|
||||
|
||||
excludedCell?.forValue((row, column) => {
|
||||
if (row >= rangeStartRow && row <= rangeEndRow && column >= rangeStartColumn && column <= rangeEndColumn) {
|
||||
isInclude = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isInclude) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// pushChildren(tree: FormulaDependencyTree) {
|
||||
// this.children.add(tree.treeId);
|
||||
// tree._pushParent(this);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Add the range corresponding to the current ast node.
|
||||
* @param range
|
||||
*/
|
||||
pushRangeList(ranges: IUnitRange[]) {
|
||||
this.rangeList.push(...ranges);
|
||||
}
|
||||
|
||||
shouldBePushRangeList() {
|
||||
return this.rangeList.length === 0 && this.type !== FormulaDependencyTreeType.FEATURE_FORMULA;
|
||||
}
|
||||
|
||||
// hasChildren(treeId: number) {
|
||||
// return this.children.has(treeId);
|
||||
// }
|
||||
|
||||
toRTreeItem(): IUnitRange[] {
|
||||
if (this.featureId != null) {
|
||||
return this.featureDirtyRanges;
|
||||
}
|
||||
|
||||
const currentRow = this.row;
|
||||
const currentColumn = this.column;
|
||||
|
||||
return [{
|
||||
unitId: this.unitId,
|
||||
sheetId: this.subUnitId,
|
||||
range: {
|
||||
startRow: currentRow,
|
||||
startColumn: currentColumn,
|
||||
endRow: currentRow,
|
||||
endColumn: currentColumn,
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether it is dependent on other trees.
|
||||
* @param dependenceTree
|
||||
*/
|
||||
// dependency(dependenceTree: FormulaDependencyTree) {
|
||||
// if (this.rangeList.length === 0) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// for (let r = 0, len = this.rangeList.length; r < len; r++) {
|
||||
// const unitRange = this.rangeList[r];
|
||||
// const unitId = unitRange.unitId;
|
||||
// const sheetId = unitRange.sheetId;
|
||||
// const range = unitRange.range;
|
||||
|
||||
// if (
|
||||
// dependenceTree.unitId === unitId &&
|
||||
// dependenceTree.subUnitId === sheetId &&
|
||||
// dependenceTree.inRangeData(range)
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// private _pushParent(tree: FormulaDependencyTree) {
|
||||
// this.parents.add(tree.treeId);
|
||||
// }
|
||||
}
|
||||
|
||||
interface IFormulaDependencyTreeJsonBase {
|
||||
treeId: number;
|
||||
formula: string;
|
||||
row: number;
|
||||
column: number;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
refOffsetX: number;
|
||||
refOffsetY: number;
|
||||
rangeList: IUnitRange[];
|
||||
refTreeId: number | undefined;
|
||||
formulaId: Nullable<string>;
|
||||
featureId: Nullable<string>;
|
||||
type: Nullable<FormulaDependencyTreeType>;
|
||||
}
|
||||
|
||||
export interface IFormulaDependencyTreeJson extends IFormulaDependencyTreeJsonBase {
|
||||
children: number[];
|
||||
parents: number[];
|
||||
}
|
||||
|
||||
export interface IFormulaDependencyTreeFullJson extends IFormulaDependencyTreeJsonBase {
|
||||
children: IFormulaDependencyTreeJson[];
|
||||
parents: IFormulaDependencyTreeJson[];
|
||||
}
|
||||
|
||||
export interface IFormulaDependentsAndInRangeResults {
|
||||
dependents: IFormulaDependencyTreeJson[];
|
||||
inRanges: IFormulaDependencyTreeJson[];
|
||||
}
|
||||
|
||||
export class FormulaDependencyTreeModel {
|
||||
children: Set<FormulaDependencyTreeModel> = new Set();
|
||||
|
||||
parents: Set<FormulaDependencyTreeModel> = new Set();
|
||||
|
||||
treeId: number = -1;
|
||||
|
||||
formula: string = '';
|
||||
|
||||
refOffsetX: number = 0;
|
||||
|
||||
refOffsetY: number = 0;
|
||||
|
||||
row: number = -1;
|
||||
|
||||
column: number = -1;
|
||||
|
||||
unitId: string = '';
|
||||
|
||||
subUnitId: string = '';
|
||||
|
||||
rangeList: IUnitRange[] = [];
|
||||
|
||||
refTreeId: number | undefined;
|
||||
|
||||
formulaId: Nullable<string>;
|
||||
|
||||
featureId: Nullable<string>;
|
||||
|
||||
type: Nullable<FormulaDependencyTreeType>;
|
||||
|
||||
constructor(tree: IFormulaDependencyTree) {
|
||||
this.treeId = tree.treeId;
|
||||
this.row = tree.row;
|
||||
this.column = tree.column;
|
||||
this.unitId = tree.unitId;
|
||||
this.subUnitId = tree.subUnitId;
|
||||
this.refOffsetX = tree.refOffsetX;
|
||||
this.refOffsetY = tree.refOffsetY;
|
||||
this.rangeList = tree.rangeList;
|
||||
|
||||
if (tree.isVirtual) {
|
||||
this.refTreeId = (tree as FormulaDependencyTreeVirtual).refTree?.treeId ?? -1;
|
||||
} else {
|
||||
this.formula = tree.formula;
|
||||
this.formulaId = (tree as FormulaDependencyTree).formulaId ?? undefined;
|
||||
this.featureId = (tree as FormulaDependencyTree).featureId ?? undefined;
|
||||
this.type = (tree as FormulaDependencyTree).type;
|
||||
}
|
||||
}
|
||||
|
||||
toJson(): IFormulaDependencyTreeJson {
|
||||
return {
|
||||
children: Array.from(this.children).map((child) => child.treeId),
|
||||
parents: Array.from(this.parents).map((parent) => parent.treeId),
|
||||
treeId: this.treeId,
|
||||
formula: this.formula,
|
||||
row: this.row,
|
||||
column: this.column,
|
||||
unitId: this.unitId,
|
||||
subUnitId: this.subUnitId,
|
||||
refOffsetX: this.refOffsetX,
|
||||
refOffsetY: this.refOffsetY,
|
||||
rangeList: this.rangeList,
|
||||
refTreeId: this.refTreeId,
|
||||
formulaId: this.formulaId,
|
||||
featureId: this.featureId,
|
||||
type: this.type,
|
||||
};
|
||||
}
|
||||
|
||||
toFullJson(): IFormulaDependencyTreeFullJson {
|
||||
return {
|
||||
children: Array.from(this.children).map((child) => child.toJson()),
|
||||
parents: Array.from(this.parents).map((parent) => parent.toJson()),
|
||||
treeId: this.treeId,
|
||||
formula: this.formula,
|
||||
row: this.row,
|
||||
column: this.column,
|
||||
unitId: this.unitId,
|
||||
subUnitId: this.subUnitId,
|
||||
refOffsetX: this.refOffsetX,
|
||||
refOffsetY: this.refOffsetY,
|
||||
rangeList: this.rangeList,
|
||||
refTreeId: this.refTreeId,
|
||||
formulaId: this.formulaId,
|
||||
featureId: this.featureId,
|
||||
type: this.type,
|
||||
};
|
||||
}
|
||||
|
||||
addParent(parent: FormulaDependencyTreeModel) {
|
||||
this.parents.add(parent);
|
||||
}
|
||||
|
||||
addChild(child: FormulaDependencyTreeModel) {
|
||||
this.children.add(child);
|
||||
}
|
||||
}
|
||||
1597
packages/engine-formula/src/engine/dependency/formula-dependency.ts
Normal file
1597
packages/engine-formula/src/engine/dependency/formula-dependency.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 { IRange } from '@univerjs/core';
|
||||
|
||||
// // the size of the block
|
||||
// const BLOCKSIZE = 10;
|
||||
|
||||
// export function setRangeBlockToken(range: IRange, baseKey: string, token: string, map: Map<string, Map<string, Set<string>>>): void {
|
||||
// const { startRow, startColumn, endRow, endColumn } = range;
|
||||
|
||||
// // Calculate the start and end index of the block
|
||||
// const startH = Math.floor(startRow / BLOCKSIZE);
|
||||
// const endH = Math.ceil(endRow / BLOCKSIZE);
|
||||
// const startV = Math.floor(startColumn / BLOCKSIZE);
|
||||
// const endV = Math.ceil(endColumn / BLOCKSIZE);
|
||||
|
||||
// // Iterate over all blocks that intersect with the range
|
||||
// for (let x = startH; x <= endH; x++) {
|
||||
// for (let y = startV; y <= endV; y++) {
|
||||
// const key = `${baseKey}_${x}_${y}`;
|
||||
// if (!map.has(baseKey)) {
|
||||
// map.set(baseKey, new Map());
|
||||
// }
|
||||
// if (!map.get(baseKey)!.has(key)) {
|
||||
// map.get(baseKey)!.set(key, new Set<string>());
|
||||
// }
|
||||
// map.get(baseKey)!.get(key)!.add(token);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// export function getBlockTokensByRange(tokensSet: Set<string>, range: IRange, baseKey: string, map: Map<string, Map<string, Set<string>>>): Set<string> {
|
||||
// const { startRow, startColumn, endRow, endColumn } = range;
|
||||
|
||||
// // Calculate the start and end index of the block
|
||||
// const startH = Math.floor(startRow / BLOCKSIZE);
|
||||
// const endH = Math.floor(endRow / BLOCKSIZE);
|
||||
// const startV = Math.floor(startColumn / BLOCKSIZE);
|
||||
// const endV = Math.floor(endColumn / BLOCKSIZE);
|
||||
|
||||
// // Iterate over all blocks that intersect with the range
|
||||
// for (let x = startH; x <= endH; x++) {
|
||||
// for (let y = startV; y <= endV; y++) {
|
||||
// const key = `${baseKey}_${x}_${y}`;
|
||||
// if (map.has(baseKey) && map.get(baseKey)!.has(key)) {
|
||||
// const blockTokens = map.get(baseKey)!.get(key)!;
|
||||
// blockTokens.forEach((token) => tokensSet.add(token));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return tokensSet;
|
||||
// }
|
||||
|
||||
// export function updateBlockKey(tokensSet: Map<string, Set<string>>, range: IRange, baseKey: string, map: Map<string, Map<string, Set<string>>>) {
|
||||
// const { startRow, startColumn, endRow, endColumn } = range;
|
||||
|
||||
// // Calculate the start and end index of the block
|
||||
// const startH = Math.floor(startRow / BLOCKSIZE);
|
||||
// const endH = Math.floor(endRow / BLOCKSIZE);
|
||||
// const startV = Math.floor(startColumn / BLOCKSIZE);
|
||||
// const endV = Math.floor(endColumn / BLOCKSIZE);
|
||||
|
||||
// let hasCreate = tokensSet.has(baseKey);
|
||||
|
||||
// // Iterate over all blocks that intersect with the range
|
||||
// for (let x = startH; x <= endH; x++) {
|
||||
// for (let y = startV; y <= endV; y++) {
|
||||
// if (!hasCreate) {
|
||||
// tokensSet.set(baseKey, new Set<string>());
|
||||
// hasCreate = true;
|
||||
// }
|
||||
|
||||
// tokensSet.get(baseKey)?.add(`${baseKey}_${x}_${y}`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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, vi } from 'vitest';
|
||||
import { AstNodePromiseType } from '../../../basics/common';
|
||||
import { createFunctionTestBed } from '../../../functions/__tests__/create-function-test-bed';
|
||||
import { BaseAstNode } from '../../ast-node/base-ast-node';
|
||||
import { NumberValueObject } from '../../value-object/primitive-object';
|
||||
import { Interpreter } from '../interpreter';
|
||||
|
||||
class AsyncReferenceLikeNode extends BaseAstNode {
|
||||
readonly executeAsyncSpy = vi.fn(async () => {
|
||||
this.setValue(NumberValueObject.create(42));
|
||||
return AstNodePromiseType.SUCCESS;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super('[Remote]Sheet1!A1');
|
||||
this.setAsync();
|
||||
}
|
||||
|
||||
override executeAsync() {
|
||||
return this.executeAsyncSpy();
|
||||
}
|
||||
}
|
||||
|
||||
describe('Interpreter async nodes', () => {
|
||||
it('detects and awaits an async root node regardless of node type', async () => {
|
||||
const testBed = createFunctionTestBed();
|
||||
const interpreter = testBed.get(Interpreter);
|
||||
const node = new AsyncReferenceLikeNode();
|
||||
|
||||
expect(interpreter.checkAsyncNode(node)).toBe(true);
|
||||
const value = await interpreter.executeAsync({ node, refOffsetX: 0, refOffsetY: 0 });
|
||||
|
||||
expect(node.executeAsyncSpy).toHaveBeenCalledOnce();
|
||||
expect((value as NumberValueObject).getValue()).toBe(42);
|
||||
});
|
||||
});
|
||||
498
packages/engine-formula/src/engine/interpreter/interpreter.ts
Normal file
498
packages/engine-formula/src/engine/interpreter/interpreter.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* 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 { DateSystem, Nullable } from '@univerjs/core';
|
||||
import type { BaseAstNode } from '../ast-node/base-ast-node';
|
||||
import type { LambdaNode } from '../ast-node/lambda-node';
|
||||
import type { ReferenceNode } from '../ast-node/reference-node';
|
||||
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
|
||||
import type { IExecuteAstNodeData } from '../utils/ast-node-tool';
|
||||
import type { PreCalculateNodeType } from '../utils/node-type';
|
||||
import type { ArrayValueObject } from '../value-object/array-value-object';
|
||||
import type { BaseValueObject } from '../value-object/base-value-object';
|
||||
import { Disposable } from '@univerjs/core';
|
||||
import { AstNodePromiseType } from '../../basics/common';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME } from '../../basics/token-type';
|
||||
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
|
||||
import { IFormulaRuntimeService } from '../../services/runtime.service';
|
||||
import { NodeType } from '../ast-node/node-type';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BooleanValueObject, NumberValueObject } from '../value-object/primitive-object';
|
||||
|
||||
type InterpreterRuntimeService = Pick<
|
||||
IFormulaRuntimeService,
|
||||
'currentColumn' | 'currentRow' | 'currentUnitId' | 'isStopExecution'
|
||||
>;
|
||||
|
||||
export class Interpreter extends Disposable {
|
||||
constructor(
|
||||
@IFormulaRuntimeService private readonly _runtimeService: InterpreterRuntimeService,
|
||||
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async executeAsync(nodeData: IExecuteAstNodeData): Promise<FunctionVariantType> {
|
||||
// if (!this._interpreterCalculateProps) {
|
||||
// return ErrorValueObject.create(ErrorType.ERROR);
|
||||
// }
|
||||
|
||||
if (!nodeData || !nodeData.node) {
|
||||
return Promise.resolve(ErrorValueObject.create(ErrorType.VALUE));
|
||||
}
|
||||
|
||||
const node = nodeData.node;
|
||||
const refOffsetX = nodeData.refOffsetX;
|
||||
const refOffsetY = nodeData.refOffsetY;
|
||||
|
||||
await this._executeAsync(node, this.getDateSystem(), refOffsetX, refOffsetY);
|
||||
|
||||
const value = node.getValue();
|
||||
|
||||
if (value == null) {
|
||||
return Promise.resolve(ErrorValueObject.create(ErrorType.VALUE));
|
||||
}
|
||||
|
||||
return Promise.resolve(value);
|
||||
}
|
||||
|
||||
execute(nodeData: IExecuteAstNodeData): FunctionVariantType {
|
||||
// if (!this._interpreterCalculateProps) {
|
||||
// return ErrorValueObject.create(ErrorType.ERROR);
|
||||
// }
|
||||
|
||||
if (!nodeData || !nodeData.node) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
const node = nodeData.node;
|
||||
const refOffsetX = nodeData.refOffsetX;
|
||||
const refOffsetY = nodeData.refOffsetY;
|
||||
|
||||
this._execute(node, this.getDateSystem(), refOffsetX, refOffsetY);
|
||||
|
||||
const value = node.getValue();
|
||||
|
||||
if (value == null) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
executePreCalculateNode(node: PreCalculateNodeType) {
|
||||
node.execute(this.getDateSystem());
|
||||
return node.getValue();
|
||||
}
|
||||
|
||||
checkAsyncNode(node: Nullable<BaseAstNode>) {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
if (node.isAsync()) {
|
||||
return true;
|
||||
}
|
||||
const result: boolean[] = [];
|
||||
this._checkAsyncNode(node, result);
|
||||
|
||||
for (let i = 0, len = result.length; i < len; i++) {
|
||||
const item = result[i];
|
||||
if (item === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _checkAsyncNode(node: BaseAstNode, resultList: boolean[]) {
|
||||
const children = node.getChildren();
|
||||
const childrenCount = children.length;
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
resultList.push(item.isAsync());
|
||||
this._checkAsyncNode(item, resultList);
|
||||
}
|
||||
}
|
||||
|
||||
private async _executeAsync(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX = 0,
|
||||
refOffsetY = 0
|
||||
): Promise<AstNodePromiseType> {
|
||||
if (this._runtimeService.isStopExecution()) {
|
||||
return Promise.resolve(AstNodePromiseType.ERROR);
|
||||
}
|
||||
if (await this._executeLazyFunctionAsync(node, dateSystem, refOffsetX, refOffsetY)) {
|
||||
return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
}
|
||||
const children = node.getChildren();
|
||||
const childrenCount = children.length;
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
const token = item.getToken();
|
||||
/**
|
||||
* If the lambda expression has no parameters, then do not execute further.
|
||||
*/
|
||||
if (
|
||||
token.toUpperCase() === DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME &&
|
||||
(item as LambdaNode).isEmptyParamFunction()
|
||||
) {
|
||||
item.execute(dateSystem);
|
||||
continue;
|
||||
}
|
||||
await this._executeAsync(item, dateSystem, refOffsetX, refOffsetY);
|
||||
}
|
||||
|
||||
if (node.nodeType === NodeType.REFERENCE) {
|
||||
(node as ReferenceNode).setRefOffset(refOffsetX, refOffsetY);
|
||||
}
|
||||
|
||||
if (node.isAsync()) {
|
||||
await node.executeAsync(dateSystem);
|
||||
} else {
|
||||
node.execute(dateSystem);
|
||||
}
|
||||
|
||||
return Promise.resolve(AstNodePromiseType.SUCCESS);
|
||||
}
|
||||
|
||||
private _execute(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX = 0,
|
||||
refOffsetY = 0
|
||||
): AstNodePromiseType {
|
||||
if (this._runtimeService.isStopExecution()) {
|
||||
return AstNodePromiseType.ERROR;
|
||||
}
|
||||
if (this._executeLazyFunction(node, dateSystem, refOffsetX, refOffsetY)) {
|
||||
return AstNodePromiseType.SUCCESS;
|
||||
}
|
||||
const children = node.getChildren();
|
||||
const childrenCount = children.length;
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
const item = children[i];
|
||||
const token = item.getToken();
|
||||
/**
|
||||
* If the lambda expression has no parameters, then do not execute further.
|
||||
*/
|
||||
if (
|
||||
token.toUpperCase() === DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME &&
|
||||
(item as LambdaNode).isEmptyParamFunction()
|
||||
) {
|
||||
item.execute(dateSystem);
|
||||
continue;
|
||||
}
|
||||
this._execute(item, dateSystem, refOffsetX, refOffsetY);
|
||||
}
|
||||
|
||||
if (node.nodeType === NodeType.REFERENCE) {
|
||||
(node as ReferenceNode).setRefOffset(refOffsetX, refOffsetY);
|
||||
}
|
||||
|
||||
node.execute(dateSystem);
|
||||
|
||||
return AstNodePromiseType.SUCCESS;
|
||||
}
|
||||
|
||||
private async _executeLazyFunctionAsync(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX: number,
|
||||
refOffsetY: number
|
||||
): Promise<boolean> {
|
||||
if (node.nodeType !== NodeType.FUNCTION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const token = node.getToken().toUpperCase();
|
||||
if (token === 'IF') {
|
||||
return await this._executeLazyIfAsync(node, dateSystem, refOffsetX, refOffsetY);
|
||||
}
|
||||
if (token === 'IFERROR') {
|
||||
return await this._executeLazyIfErrorAsync(node, dateSystem, refOffsetX, refOffsetY);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _executeLazyFunction(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX: number,
|
||||
refOffsetY: number
|
||||
): boolean {
|
||||
if (node.nodeType !== NodeType.FUNCTION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const token = node.getToken().toUpperCase();
|
||||
if (token === 'IF') {
|
||||
return this._executeLazyIf(node, dateSystem, refOffsetX, refOffsetY);
|
||||
}
|
||||
if (token === 'IFERROR') {
|
||||
return this._executeLazyIfError(node, dateSystem, refOffsetX, refOffsetY);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _executeLazyIfAsync(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX: number,
|
||||
refOffsetY: number
|
||||
): Promise<boolean> {
|
||||
const children = node.getChildren();
|
||||
if (children.length < 2 || children.length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this._executeAsync(children[0], dateSystem, refOffsetX, refOffsetY);
|
||||
const condition = children[0].getValue();
|
||||
if (condition == null || condition.isReferenceObject() || condition.isArray()) {
|
||||
return false;
|
||||
}
|
||||
if (condition.isError()) {
|
||||
node.setValue(condition);
|
||||
return true;
|
||||
}
|
||||
|
||||
const selected = (condition as BaseValueObject).getValue() ? children[1] : children[2];
|
||||
if (selected == null) {
|
||||
node.setValue(BooleanValueObject.create(false));
|
||||
return true;
|
||||
}
|
||||
|
||||
await this._executeAsync(selected, dateSystem, refOffsetX, refOffsetY);
|
||||
node.setValue(this._toLazyIfSelectedValue(selected.getValue(), node));
|
||||
return true;
|
||||
}
|
||||
|
||||
private _executeLazyIf(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX: number,
|
||||
refOffsetY: number
|
||||
): boolean {
|
||||
const children = node.getChildren();
|
||||
if (children.length < 2 || children.length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._execute(children[0], dateSystem, refOffsetX, refOffsetY);
|
||||
const condition = children[0].getValue();
|
||||
if (condition == null || condition.isReferenceObject() || condition.isArray()) {
|
||||
return false;
|
||||
}
|
||||
if (condition.isError()) {
|
||||
node.setValue(condition);
|
||||
return true;
|
||||
}
|
||||
|
||||
const selected = (condition as BaseValueObject).getValue() ? children[1] : children[2];
|
||||
if (selected == null) {
|
||||
node.setValue(BooleanValueObject.create(false));
|
||||
return true;
|
||||
}
|
||||
|
||||
this._execute(selected, dateSystem, refOffsetX, refOffsetY);
|
||||
node.setValue(this._toLazyIfSelectedValue(selected.getValue(), node));
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _executeLazyIfErrorAsync(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX: number,
|
||||
refOffsetY: number
|
||||
): Promise<boolean> {
|
||||
const children = node.getChildren();
|
||||
if (children.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this._executeAsync(children[0], dateSystem, refOffsetX, refOffsetY);
|
||||
const rawValue = children[0].getValue();
|
||||
const value = this._toLazyIfErrorValue(rawValue);
|
||||
if (value == null || value.isArray()) {
|
||||
return false;
|
||||
}
|
||||
if (!value.isError()) {
|
||||
node.setValue(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
await this._executeAsync(children[1], dateSystem, refOffsetX, refOffsetY);
|
||||
node.setValue(children[1].getValue() ?? ErrorValueObject.create(ErrorType.VALUE));
|
||||
return true;
|
||||
}
|
||||
|
||||
private _executeLazyIfError(
|
||||
node: BaseAstNode,
|
||||
dateSystem: DateSystem,
|
||||
refOffsetX: number,
|
||||
refOffsetY: number
|
||||
): boolean {
|
||||
const children = node.getChildren();
|
||||
if (children.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._execute(children[0], dateSystem, refOffsetX, refOffsetY);
|
||||
const rawValue = children[0].getValue();
|
||||
const value = this._toLazyIfErrorValue(rawValue);
|
||||
if (value == null || value.isArray()) {
|
||||
return false;
|
||||
}
|
||||
if (!value.isError()) {
|
||||
node.setValue(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._execute(children[1], dateSystem, refOffsetX, refOffsetY);
|
||||
node.setValue(children[1].getValue() ?? ErrorValueObject.create(ErrorType.VALUE));
|
||||
return true;
|
||||
}
|
||||
|
||||
getDateSystem(): DateSystem {
|
||||
return this._currentConfigService.getDateSystem(this._runtimeService.currentUnitId);
|
||||
}
|
||||
|
||||
private _toLazyIfErrorValue(value: Nullable<FunctionVariantType>): Nullable<BaseValueObject> {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!value.isReferenceObject()) {
|
||||
const baseValue = value as BaseValueObject;
|
||||
return baseValue.isNull() ? NumberValueObject.create(0) : baseValue;
|
||||
}
|
||||
|
||||
const reference = value as BaseReferenceObject;
|
||||
|
||||
if (reference.getRowCount() !== 1 || reference.getColumnCount() !== 1) {
|
||||
return reference.toArrayValueObject();
|
||||
}
|
||||
|
||||
const cellValue = reference.getCellByPosition();
|
||||
return cellValue.isNull() ? NumberValueObject.create(0) : cellValue;
|
||||
}
|
||||
|
||||
private _toLazyIfSelectedValue(value: Nullable<FunctionVariantType>, node: BaseAstNode): FunctionVariantType {
|
||||
if (value == null) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
const preserveArray = this._preserveLazyIfSelectedReferenceArray(node);
|
||||
if (value.isReferenceObject()) {
|
||||
const reference = value as BaseReferenceObject;
|
||||
if (preserveArray) {
|
||||
return reference.toArrayValueObject();
|
||||
}
|
||||
value = this._implicitLazyIfReferenceValue(reference);
|
||||
}
|
||||
|
||||
if (value.isArray() && !preserveArray) {
|
||||
value = this._implicitLazyIfArrayValue(value as ArrayValueObject);
|
||||
}
|
||||
|
||||
if (!value.isArray()) {
|
||||
const baseValue = value as BaseValueObject;
|
||||
return baseValue.isNull() ? NumberValueObject.create(0) : baseValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private _implicitLazyIfReferenceValue(reference: BaseReferenceObject): BaseValueObject {
|
||||
const range = reference.getRangePosition();
|
||||
const currentRow = this._runtimeService.currentRow || 0;
|
||||
const currentColumn = this._runtimeService.currentColumn || 0;
|
||||
const { startRow, startColumn, endRow, endColumn } = range;
|
||||
|
||||
if (startRow === endRow && startColumn === endColumn) {
|
||||
return reference.getCellByPosition();
|
||||
}
|
||||
|
||||
if (endRow === startRow && currentColumn >= startColumn && currentColumn <= endColumn) {
|
||||
return reference.getCellByColumn(currentColumn);
|
||||
}
|
||||
|
||||
if (startColumn === endColumn && currentRow >= startRow && currentRow <= endRow) {
|
||||
return reference.getCellByRow(currentRow);
|
||||
}
|
||||
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
private _preserveLazyIfSelectedReferenceArray(node: BaseAstNode): boolean {
|
||||
let child: BaseAstNode = node;
|
||||
let parent = child.getParent();
|
||||
|
||||
while (parent != null) {
|
||||
if (parent.nodeType === NodeType.FUNCTION) {
|
||||
return parent.preservesLazyIfReferenceArray(child);
|
||||
}
|
||||
|
||||
child = parent;
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _implicitLazyIfArrayValue(array: ArrayValueObject): FunctionVariantType {
|
||||
if (
|
||||
array.getUnitId() === '' ||
|
||||
array.getSheetId() === '' ||
|
||||
array.getCurrentRow() < 0 ||
|
||||
array.getCurrentColumn() < 0
|
||||
) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const startRow = array.getCurrentRow();
|
||||
const startColumn = array.getCurrentColumn();
|
||||
const rowCount = array.getRowCount();
|
||||
const columnCount = array.getColumnCount();
|
||||
const currentRow = this._runtimeService.currentRow || 0;
|
||||
const currentColumn = this._runtimeService.currentColumn || 0;
|
||||
|
||||
let rowIndex = -1;
|
||||
let columnIndex = -1;
|
||||
if (rowCount === 1 && columnCount === 1) {
|
||||
rowIndex = 0;
|
||||
columnIndex = 0;
|
||||
} else if (rowCount === 1) {
|
||||
rowIndex = 0;
|
||||
columnIndex = currentColumn - startColumn;
|
||||
} else if (columnCount === 1) {
|
||||
rowIndex = currentRow - startRow;
|
||||
columnIndex = 0;
|
||||
} else {
|
||||
rowIndex = currentRow - startRow;
|
||||
columnIndex = currentColumn - startColumn;
|
||||
}
|
||||
|
||||
if (rowIndex < 0 || rowIndex >= rowCount || columnIndex < 0 || columnIndex >= columnCount) {
|
||||
return ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
|
||||
return (array.get(rowIndex, columnIndex) as BaseValueObject) || ErrorValueObject.create(ErrorType.VALUE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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 { CellValueType, ObjectMatrix } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CellReferenceObject } from '../cell-reference-object';
|
||||
import { RangeReferenceObject } from '../range-reference-object';
|
||||
|
||||
describe('CellReferenceObject', () => {
|
||||
it('should allow blank cells inside the Excel grid even when they are outside snapshot row data', () => {
|
||||
const reference = new CellReferenceObject('Sheet2!A151');
|
||||
|
||||
reference.setForcedSheetIdDirect('sheet-2');
|
||||
|
||||
expect(reference.isExceedRange()).toBe(false);
|
||||
});
|
||||
|
||||
it('should still reject cells outside the Excel grid', () => {
|
||||
const reference = new CellReferenceObject('Sheet2!A1048577');
|
||||
|
||||
reference.setForcedSheetIdDirect('sheet-2');
|
||||
|
||||
expect(reference.isExceedRange()).toBe(true);
|
||||
});
|
||||
|
||||
it('should still reject unresolved forced sheet names', () => {
|
||||
const reference = new CellReferenceObject('Sheet2!A151');
|
||||
|
||||
expect(reference.isExceedRange()).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve typed empty strings while iterating references', () => {
|
||||
const reference = new CellReferenceObject('A1:B1');
|
||||
reference.setDefaultUnitId('unit-1');
|
||||
reference.setDefaultSheetId('sheet-1');
|
||||
reference.setUnitData({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
rowCount: 1,
|
||||
columnCount: 2,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
cellData: new ObjectMatrix({
|
||||
0: {
|
||||
0: { v: '', t: CellValueType.STRING },
|
||||
1: { v: '' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const values: unknown[] = [];
|
||||
reference.iterator((valueObject) => {
|
||||
values.push(valueObject?.getValue() ?? null);
|
||||
});
|
||||
|
||||
expect(values).toEqual(['', null]);
|
||||
});
|
||||
|
||||
it('should return null value objects for missing cells inside the grid', () => {
|
||||
const reference = new CellReferenceObject('A1');
|
||||
reference.setDefaultUnitId('unit-1');
|
||||
reference.setDefaultSheetId('sheet-1');
|
||||
reference.setUnitData({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
rowCount: 1,
|
||||
columnCount: 1,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
cellData: new ObjectMatrix({}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(reference.getCellByPosition().isNull()).toBe(true);
|
||||
expect(reference.getCellByPosition().getValue()).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply shared-formula offsets when reading the referenced cell', () => {
|
||||
const reference = new CellReferenceObject('A1');
|
||||
reference.setDefaultUnitId('unit-1');
|
||||
reference.setDefaultSheetId('sheet-1');
|
||||
reference.setUnitData({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
rowCount: 2,
|
||||
columnCount: 1,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
cellData: new ObjectMatrix({
|
||||
0: { 0: { v: 1, t: CellValueType.NUMBER } },
|
||||
1: { 0: { v: 2, t: CellValueType.NUMBER } },
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
reference.setRefOffset(0, 1);
|
||||
|
||||
expect(reference.getCellByPosition().getValue()).toBe(2);
|
||||
expect(reference.getCellByPosition(0, 0).getValue()).toBe(1);
|
||||
});
|
||||
|
||||
it('should not apply a shared-formula offset twice when unioning a materialized reference', () => {
|
||||
const sharedReference = new CellReferenceObject('$B$2');
|
||||
const materializedReference = new CellReferenceObject('B3');
|
||||
sharedReference.setRefOffset(0, 1);
|
||||
|
||||
const union = sharedReference.unionBy(materializedReference);
|
||||
|
||||
if (!(union instanceof RangeReferenceObject)) {
|
||||
throw new TypeError('Expected a range reference');
|
||||
}
|
||||
|
||||
expect(union.getRangePosition()).toEqual(expect.objectContaining({
|
||||
startRow: 1,
|
||||
endRow: 2,
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should apply matching shared-formula offsets once when unioning references', () => {
|
||||
const startReference = new CellReferenceObject('A1');
|
||||
const endReference = new CellReferenceObject('B1');
|
||||
startReference.setRefOffset(0, 1);
|
||||
endReference.setRefOffset(0, 1);
|
||||
|
||||
const union = startReference.unionBy(endReference);
|
||||
|
||||
if (!(union instanceof RangeReferenceObject)) {
|
||||
throw new TypeError('Expected a range reference');
|
||||
}
|
||||
|
||||
expect(union.getRangePosition()).toEqual(expect.objectContaining({
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should treat string enum number cell types as numeric cells', () => {
|
||||
const reference = new CellReferenceObject('A1');
|
||||
|
||||
const valueObject = reference.getCellValueObject({ v: '43497', t: String(CellValueType.NUMBER) as never });
|
||||
|
||||
expect(valueObject.isNumber()).toBe(true);
|
||||
expect(valueObject.getValue()).toBe(43497);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { BaseValueObject } from '../../value-object/base-value-object';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ErrorType } from '../../../basics/error-type';
|
||||
import { createNewArray } from '../../utils/array-object';
|
||||
import { ErrorValueObject } from '../../value-object/base-value-object';
|
||||
import { NumberValueObject } from '../../value-object/primitive-object';
|
||||
import { BaseReferenceObject } from '../base-reference-object';
|
||||
import { MultiAreaArrayMode, MultiAreaReferenceObject } from '../multi-area-reference-object';
|
||||
|
||||
interface ITestReferenceConfig {
|
||||
rowCount?: number;
|
||||
columnCount?: number;
|
||||
exceed?: boolean;
|
||||
unitId?: string;
|
||||
sheetId?: string;
|
||||
range?: { startRow: number; startColumn: number; endRow: number; endColumn: number };
|
||||
rangeData?: { startRow: number; startColumn: number; endRow: number; endColumn: number };
|
||||
iteratorValue?: number;
|
||||
}
|
||||
|
||||
class TestReferenceObject extends BaseReferenceObject {
|
||||
override readonly dispose = vi.fn(() => super.dispose());
|
||||
override readonly setRefOffset = vi.fn((x = 0, y = 0) => super.setRefOffset(x, y));
|
||||
|
||||
constructor(private readonly _config: ITestReferenceConfig = {}) {
|
||||
super('test-reference');
|
||||
}
|
||||
|
||||
override getRowCount(): number {
|
||||
return this._config.rowCount ?? 1;
|
||||
}
|
||||
|
||||
override getColumnCount(): number {
|
||||
return this._config.columnCount ?? 1;
|
||||
}
|
||||
|
||||
override isExceedRange(): boolean {
|
||||
return this._config.exceed ?? false;
|
||||
}
|
||||
|
||||
override getUnitId(): string {
|
||||
return this._config.unitId ?? 'unit-1';
|
||||
}
|
||||
|
||||
override getSheetId(): string {
|
||||
return this._config.sheetId ?? 'sheet-1';
|
||||
}
|
||||
|
||||
override getActiveSheetRowCount(): number {
|
||||
return 100;
|
||||
}
|
||||
|
||||
override getActiveSheetColumnCount(): number {
|
||||
return 26;
|
||||
}
|
||||
|
||||
override iterator(
|
||||
callback: (value: Nullable<BaseValueObject>, row: number, column: number) => Nullable<boolean>
|
||||
): void {
|
||||
callback(NumberValueObject.create(this._config.iteratorValue ?? 1), 0, 0);
|
||||
}
|
||||
|
||||
override getFirstCell(): BaseValueObject {
|
||||
return NumberValueObject.create(this._config.iteratorValue ?? 1);
|
||||
}
|
||||
|
||||
override getRangePosition() {
|
||||
return this._config.range ?? { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 };
|
||||
}
|
||||
|
||||
override getRangeData() {
|
||||
return this._config.rangeData ?? this.getRangePosition();
|
||||
}
|
||||
|
||||
override toArrayValueObject() {
|
||||
const rowCount = this.getRowCount();
|
||||
const columnCount = this.getColumnCount();
|
||||
return createNewArray(
|
||||
Array.from(
|
||||
{ length: rowCount },
|
||||
() => Array.from(
|
||||
{ length: columnCount },
|
||||
() => NumberValueObject.create(this._config.iteratorValue ?? 1)
|
||||
)
|
||||
),
|
||||
rowCount,
|
||||
columnCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createAreaStub(config?: ITestReferenceConfig): TestReferenceObject {
|
||||
return new TestReferenceObject(config);
|
||||
}
|
||||
|
||||
function createErrorAreaStub(): ErrorValueObject {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
describe('MultiAreaReferenceObject', () => {
|
||||
it('should manage areas and multi-area flags', () => {
|
||||
const areaA = createAreaStub({ rowCount: 2, columnCount: 3 });
|
||||
const areaB = createAreaStub({ rowCount: 4, columnCount: 5 });
|
||||
const multi = new MultiAreaReferenceObject('token', [[areaA]]);
|
||||
|
||||
multi.addArea(areaB);
|
||||
multi.addArea([areaA, areaB]);
|
||||
|
||||
expect(multi.isMultiArea()).toBe(true);
|
||||
expect(multi.isRange()).toBe(false);
|
||||
expect(multi.isCell()).toBe(false);
|
||||
expect(multi.isRow()).toBe(false);
|
||||
expect(multi.isColumn()).toBe(false);
|
||||
expect(multi.getAreas().length).toBe(3);
|
||||
expect(multi.getRowCount()).toBe(3);
|
||||
expect(multi.getColumnCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('should ignore error areas in count, range and sheet inference', () => {
|
||||
const area = createAreaStub({
|
||||
unitId: 'unit-A',
|
||||
sheetId: 'sheet-A',
|
||||
range: { startRow: 2, startColumn: 3, endRow: 4, endColumn: 5 },
|
||||
});
|
||||
const errorArea = createErrorAreaStub();
|
||||
const multi = new MultiAreaReferenceObject('token', [[errorArea, area]]);
|
||||
|
||||
expect(multi.getUnitId()).toBe('unit-A');
|
||||
expect(multi.getSheetId()).toBe('sheet-A');
|
||||
expect(multi.getActiveSheetRowCount()).toBe(100);
|
||||
expect(multi.getActiveSheetColumnCount()).toBe(26);
|
||||
expect(multi.isExceedRange()).toBe(false);
|
||||
expect(multi.getRangePosition()).toEqual({ startRow: 2, startColumn: 3, endRow: 4, endColumn: 5 });
|
||||
});
|
||||
|
||||
it('should propagate offset and iterate in row-major order with stop signal', () => {
|
||||
const area1 = createAreaStub({ iteratorValue: 1 });
|
||||
const area2 = createAreaStub({ iteratorValue: 2 });
|
||||
const multi = new MultiAreaReferenceObject('token', [[area1, area2]]);
|
||||
|
||||
multi.setRefOffset(2, 3);
|
||||
expect(area1.setRefOffset).toHaveBeenCalledWith(2, 3);
|
||||
expect(area2.setRefOffset).toHaveBeenCalledWith(2, 3);
|
||||
|
||||
const values: number[] = [];
|
||||
multi.iterator((v) => {
|
||||
const value = v?.getValue();
|
||||
if (typeof value === 'number') {
|
||||
values.push(value);
|
||||
}
|
||||
return values.length < 1;
|
||||
});
|
||||
expect(values).toEqual([1]);
|
||||
});
|
||||
|
||||
it('should convert multi-area to array object and unit range', () => {
|
||||
const area1 = createAreaStub({
|
||||
range: { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 },
|
||||
rangeData: { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 },
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
iteratorValue: 9,
|
||||
});
|
||||
const area2 = createAreaStub({
|
||||
range: { startRow: 5, startColumn: 6, endRow: 7, endColumn: 8 },
|
||||
rangeData: { startRow: 5, startColumn: 6, endRow: 7, endColumn: 8 },
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
iteratorValue: 5,
|
||||
});
|
||||
const multi = new MultiAreaReferenceObject('token', [[area1, area2]]);
|
||||
|
||||
const array = multi.toArrayValueObject();
|
||||
expect(array.getRowCount()).toBe(1);
|
||||
expect(array.getColumnCount()).toBe(2);
|
||||
expect(array.get(0, 0)?.getValue()).toBe(9);
|
||||
expect(array.get(0, 1)?.getValue()).toBe(5);
|
||||
|
||||
expect(multi.getRangePosition()).toEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
});
|
||||
expect(multi.getRangeData()).toEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
});
|
||||
expect(multi.toUnitRange()).toEqual({
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
},
|
||||
});
|
||||
expect(multi.getFirstCell().getValue()).toBe(9);
|
||||
});
|
||||
|
||||
it('should stack every value and preserve every sheet range in stack mode', () => {
|
||||
const area1 = createAreaStub({
|
||||
rowCount: 2,
|
||||
columnCount: 2,
|
||||
unitId: 'u1',
|
||||
sheetId: 'jan',
|
||||
iteratorValue: 1,
|
||||
});
|
||||
const area2 = createAreaStub({
|
||||
rowCount: 1,
|
||||
columnCount: 2,
|
||||
unitId: 'u1',
|
||||
sheetId: 'feb',
|
||||
iteratorValue: 2,
|
||||
});
|
||||
const multi = new MultiAreaReferenceObject(
|
||||
'Jan:Feb!A1:B2',
|
||||
[[area1], [area2]],
|
||||
MultiAreaArrayMode.STACK_AREAS
|
||||
);
|
||||
|
||||
const array = multi.toArrayValueObject();
|
||||
expect(multi.getRowCount()).toBe(3);
|
||||
expect(multi.getColumnCount()).toBe(2);
|
||||
expect(array.getArrayValue().map((row) => row.map((value) => value?.getValue()))).toEqual([
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
]);
|
||||
expect(multi.toUnitRanges().map(({ sheetId }) => sheetId)).toEqual(['jan', 'feb']);
|
||||
});
|
||||
|
||||
it('should fallback to parent behavior when all areas are invalid', () => {
|
||||
const invalidArea = createAreaStub({
|
||||
range: {
|
||||
startRow: Number.POSITIVE_INFINITY,
|
||||
startColumn: Number.POSITIVE_INFINITY,
|
||||
endRow: Number.NEGATIVE_INFINITY,
|
||||
endColumn: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
rangeData: {
|
||||
startRow: Number.POSITIVE_INFINITY,
|
||||
startColumn: Number.POSITIVE_INFINITY,
|
||||
endRow: Number.NEGATIVE_INFINITY,
|
||||
endColumn: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
});
|
||||
const multi = new MultiAreaReferenceObject('token', [[invalidArea]]);
|
||||
|
||||
expect(multi.getRangePosition()).toEqual({
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
});
|
||||
expect(multi.getRangeData()).toEqual({
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispose all areas', () => {
|
||||
const area1 = createAreaStub();
|
||||
const area2 = createAreaStub();
|
||||
const multi = new MultiAreaReferenceObject('token', [[area1, area2]]);
|
||||
|
||||
multi.dispose();
|
||||
expect(area1.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(area2.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(multi.getAreas()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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([
|
||||
['#All', TableOptionType.ALL],
|
||||
['#Data', TableOptionType.DATA],
|
||||
['#Totals', TableOptionType.TOTALS],
|
||||
['#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);
|
||||
});
|
||||
|
||||
it('excludes a declared totals row from data references', () => {
|
||||
const table = {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Amount', 0]]),
|
||||
range: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 0 },
|
||||
showFooter: true,
|
||||
};
|
||||
|
||||
expect(new TableReferenceObject('Table[Amount]', table, '[Amount]', options).getRangeData()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
expect(new TableReferenceObject(
|
||||
'Table[[#Totals],[Amount]]',
|
||||
table,
|
||||
'[[#Totals],[Amount]]',
|
||||
options
|
||||
).getRangeData()).toEqual({
|
||||
startRow: 3,
|
||||
endRow: 3,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('matches CRLF structured references against LF table column names', () => {
|
||||
const reference = new TableReferenceObject('Table_FMEA[[#This Row],[F\r\n(1-5)]]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['F\n(1-5)', 3]]),
|
||||
range: { startRow: 7, endRow: 33, startColumn: 0, endColumn: 14 },
|
||||
}, '[[#This Row],[F\r\n(1-5)]]', options);
|
||||
reference.setCurrentRowAndColumn(8, 6);
|
||||
|
||||
expect(reference.getRangeData()).toEqual({
|
||||
startRow: 8,
|
||||
endRow: 8,
|
||||
startColumn: 3,
|
||||
endColumn: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats commas inside single-bracket column titles as title text', () => {
|
||||
const reference = new TableReferenceObject('Table1[Deposit,\r\nCredit (+)]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Deposit,\nCredit (+)', 6]]),
|
||||
range: { startRow: 0, endRow: 10, startColumn: 0, endColumn: 7 },
|
||||
}, '[Deposit,\r\nCredit (+)]', options);
|
||||
|
||||
expect(reference.getRangeData()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 10,
|
||||
startColumn: 6,
|
||||
endColumn: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a trailing colon inside a single-bracket column title as title text', () => {
|
||||
const reference = new TableReferenceObject('Table1[Business Name:]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Business Name:', 3]]),
|
||||
range: { startRow: 0, endRow: 10, startColumn: 0, endColumn: 6 },
|
||||
}, '[Business Name:]', options);
|
||||
|
||||
expect(reference.getRangeData()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 10,
|
||||
startColumn: 3,
|
||||
endColumn: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* 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,783 @@
|
||||
/**
|
||||
* 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, IRange, IUnitRange, Nullable } from '@univerjs/core';
|
||||
import type { IArrayFormulaRangeType, IRuntimeUnitDataType, IUnitData, IUnitSheetNameMap, IUnitStylesData } from '../../basics/common';
|
||||
import type { BaseValueObject, IArrayValueObject } from '../value-object/base-value-object';
|
||||
import { CellValueType, DateSystem, isNullCell, isTextFormat, moveRangeByOffset, ObjectMatrix } from '@univerjs/core';
|
||||
import { FormulaAstLRU } from '../../basics/cache-lru';
|
||||
import { ERROR_TYPE_SET, ErrorType } from '../../basics/error-type';
|
||||
import { ObjectClassType } from '../../basics/object-class-type';
|
||||
import { getCellValue } from '../utils/cell';
|
||||
import { getRuntimeFeatureCell } from '../utils/get-runtime-feature-cell';
|
||||
import { ArrayValueObject, ValueObjectFactory } from '../value-object/array-value-object';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import {
|
||||
createBooleanValueObjectByRawValue,
|
||||
createNumberValueObjectByRawValue,
|
||||
NullValueObject,
|
||||
NumberValueObject,
|
||||
StringValueObject,
|
||||
} from '../value-object/primitive-object';
|
||||
|
||||
export type NodeValueType = BaseValueObject | BaseReferenceObject | AsyncObject | AsyncArrayObject;
|
||||
|
||||
export type FunctionVariantType = BaseValueObject | BaseReferenceObject;
|
||||
|
||||
const FORMULA_CACHE_LRU_COUNT = 10000;
|
||||
|
||||
export const FORMULA_REF_TO_ARRAY_CACHE = new FormulaAstLRU<ArrayValueObject>(FORMULA_CACHE_LRU_COUNT);
|
||||
export class BaseReferenceObject extends ObjectClassType {
|
||||
private _dateSystem = DateSystem.Date1900;
|
||||
|
||||
private _forcedSheetId: Nullable<string> = '';
|
||||
|
||||
private _forcedSheetName: string = '';
|
||||
|
||||
private _defaultSheetId: string = '';
|
||||
|
||||
private _rangeData: IRange = {
|
||||
startColumn: -1,
|
||||
startRow: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
};
|
||||
|
||||
private _unitData: IUnitData = {};
|
||||
|
||||
private _unitStylesData: IUnitStylesData = {};
|
||||
|
||||
private _filteredOutRows: number[] = [];
|
||||
|
||||
private _defaultUnitId: string = '';
|
||||
|
||||
private _forcedUnitId: string = '';
|
||||
|
||||
private _unitQualifier: string = '';
|
||||
|
||||
private _runtimeData: IRuntimeUnitDataType = {};
|
||||
|
||||
private _arrayFormulaCellData: IRuntimeUnitDataType = {};
|
||||
|
||||
private _arrayFormulaRange: IArrayFormulaRangeType = {};
|
||||
|
||||
private _runtimeArrayFormulaCellData: IRuntimeUnitDataType = {};
|
||||
|
||||
private _runtimeArrayFormulaRange: IArrayFormulaRangeType = {};
|
||||
|
||||
private _runtimeFeatureCellData: { [featureId: string]: IRuntimeUnitDataType } = {};
|
||||
|
||||
private _refOffsetX = 0;
|
||||
|
||||
private _refOffsetY = 0;
|
||||
|
||||
private _currentRow?: number;
|
||||
|
||||
private _currentColumn?: number;
|
||||
|
||||
constructor(private _token: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._unitData = {};
|
||||
|
||||
this._unitStylesData = {};
|
||||
|
||||
this._runtimeData = {};
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this._token;
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this._token = token;
|
||||
}
|
||||
|
||||
isExceedRange() {
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
|
||||
if (startRow < 0 || startColumn < 0 || endRow >= this.getActiveSheetRowCount() || endColumn >= this.getActiveSheetColumnCount()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setRefOffset(x: number = 0, y: number = 0) {
|
||||
this._refOffsetX = x;
|
||||
this._refOffsetY = y;
|
||||
}
|
||||
|
||||
getRefOffset() {
|
||||
return {
|
||||
x: this._refOffsetX,
|
||||
y: this._refOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
getRangePosition() {
|
||||
const { x, y } = this.getRefOffset();
|
||||
const newRange = moveRangeByOffset(this.getRangeData(), x, y);
|
||||
|
||||
if (Number.isNaN(newRange.startRow)) {
|
||||
newRange.startRow = 0;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newRange.startColumn)) {
|
||||
newRange.startColumn = 0;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newRange.endRow)) {
|
||||
newRange.endRow = this.getActiveSheetRowCount() - 1;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newRange.endColumn)) {
|
||||
newRange.endColumn = this.getActiveSheetColumnCount() - 1;
|
||||
}
|
||||
|
||||
return newRange;
|
||||
}
|
||||
|
||||
override isReferenceObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getDateSystem() {
|
||||
return this._dateSystem;
|
||||
}
|
||||
|
||||
withDateSystem(dateSystem: DateSystem): this {
|
||||
this._dateSystem = dateSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
iterator(
|
||||
callback: (valueObject: Nullable<BaseValueObject>, rowIndex: number, columnIndex: number) => Nullable<boolean>
|
||||
) {
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
|
||||
if (this._checkIfWorksheetMiss()) {
|
||||
return callback(ErrorValueObject.create(ErrorType.REF), startRow, startColumn);
|
||||
}
|
||||
|
||||
const unitId = this._forcedUnitId || this._defaultUnitId;
|
||||
const sheetId = this._forcedSheetId || this._defaultSheetId;
|
||||
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
for (let c = startColumn; c <= endColumn; c++) {
|
||||
if (r < 0 || c < 0) {
|
||||
return callback(ErrorValueObject.create(ErrorType.REF), r, c);
|
||||
}
|
||||
|
||||
const cell = this.getCellData(r, c)!;
|
||||
let result: Nullable<boolean> = false;
|
||||
if (isNullCell(cell) && !isTypedEmptyStringCell(cell)) {
|
||||
result = callback(null, r, c);
|
||||
continue;
|
||||
}
|
||||
|
||||
let resultObjectValue = this.getCellValueObject(cell);
|
||||
|
||||
// Set numfmt pattern for first cell
|
||||
// TODO@Dushusir it does not have to be a number, the string can also be set a number format
|
||||
if (r === startRow && c === startColumn) {
|
||||
const pattern = this.getCellPattern(unitId, sheetId, r, c);
|
||||
if (pattern && resultObjectValue.isNumber()) {
|
||||
const value = Number(resultObjectValue.getValue());
|
||||
resultObjectValue = NumberValueObject.create(value, pattern).withDateSystem(this._dateSystem);
|
||||
}
|
||||
}
|
||||
|
||||
result = callback(resultObjectValue, r, c);
|
||||
|
||||
if (result === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFirstCell() {
|
||||
if (this._checkIfWorksheetMiss()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const { startRow, startColumn } = this.getRangePosition();
|
||||
const cell = this.getCellData(startRow, startColumn);
|
||||
|
||||
if (!cell) {
|
||||
return NumberValueObject.create(0);
|
||||
}
|
||||
|
||||
let cellValueObject = this.getCellValueObject(cell);
|
||||
|
||||
// Set numfmt pattern,
|
||||
// TODO@Dushusir it does not have to be a number, the string can also be set a number format
|
||||
const unitId = this._forcedUnitId || this._defaultUnitId;
|
||||
const sheetId = this._forcedSheetId || this._defaultSheetId;
|
||||
const pattern = this.getCellPattern(unitId, sheetId, startRow, startColumn);
|
||||
if (pattern && cellValueObject.isNumber()) {
|
||||
const value = Number(cellValueObject.getValue());
|
||||
cellValueObject = NumberValueObject.create(value, pattern).withDateSystem(this._dateSystem);
|
||||
}
|
||||
|
||||
return cellValueObject;
|
||||
}
|
||||
|
||||
getRangeData() {
|
||||
return this._rangeData;
|
||||
}
|
||||
|
||||
setRangeData(range: IRange) {
|
||||
this._rangeData = range;
|
||||
}
|
||||
|
||||
getUnitId() {
|
||||
if (this._forcedUnitId && this._forcedUnitId.length > 0) {
|
||||
return this._forcedUnitId;
|
||||
}
|
||||
return this._defaultUnitId;
|
||||
}
|
||||
|
||||
getSheetId() {
|
||||
if (this._forcedSheetId && this._forcedSheetId.length > 0) {
|
||||
return this._forcedSheetId;
|
||||
}
|
||||
return this._defaultSheetId;
|
||||
}
|
||||
|
||||
setForcedUnitIdDirect(unitId: string) {
|
||||
if (unitId.length > 0) {
|
||||
this._forcedUnitId = unitId;
|
||||
}
|
||||
}
|
||||
|
||||
setUnitQualifier(unitQualifier: string) {
|
||||
this._unitQualifier = unitQualifier;
|
||||
}
|
||||
|
||||
getUnitQualifier() {
|
||||
return this._unitQualifier;
|
||||
}
|
||||
|
||||
getForcedUnitId() {
|
||||
return this._forcedUnitId;
|
||||
}
|
||||
|
||||
setForcedSheetId(sheetNameMap: IUnitSheetNameMap) {
|
||||
if (!this._forcedSheetName) {
|
||||
return;
|
||||
}
|
||||
this._forcedSheetId = sheetNameMap[this.getUnitId()]?.[this._forcedSheetName];
|
||||
}
|
||||
|
||||
setForcedSheetIdDirect(sheetId: string) {
|
||||
this._forcedSheetId = sheetId;
|
||||
}
|
||||
|
||||
getForcedSheetId() {
|
||||
return this._forcedSheetId;
|
||||
}
|
||||
|
||||
setForcedSheetName(sheetName: string) {
|
||||
if (sheetName.length > 0) {
|
||||
this._forcedSheetName = sheetName;
|
||||
}
|
||||
}
|
||||
|
||||
getForcedSheetName() {
|
||||
return this._forcedSheetName;
|
||||
}
|
||||
|
||||
setDefaultSheetId(sheetId: string) {
|
||||
this._defaultSheetId = sheetId;
|
||||
}
|
||||
|
||||
getDefaultSheetId() {
|
||||
return this._defaultSheetId;
|
||||
}
|
||||
|
||||
setDefaultUnitId(unitId: string) {
|
||||
this._defaultUnitId = unitId;
|
||||
}
|
||||
|
||||
getDefaultUnitId() {
|
||||
return this._defaultUnitId;
|
||||
}
|
||||
|
||||
getUnitData() {
|
||||
return this._unitData;
|
||||
}
|
||||
|
||||
setUnitData(unitData: IUnitData) {
|
||||
this._unitData = unitData;
|
||||
}
|
||||
|
||||
getUnitStylesData() {
|
||||
return this._unitStylesData;
|
||||
}
|
||||
|
||||
setUnitStylesData(unitStylesData: IUnitStylesData) {
|
||||
this._unitStylesData = unitStylesData;
|
||||
}
|
||||
|
||||
getFilteredOutRows() {
|
||||
return this._filteredOutRows;
|
||||
}
|
||||
|
||||
setFilteredOutRows(filteredOutRows: number[]) {
|
||||
this._filteredOutRows = filteredOutRows;
|
||||
}
|
||||
|
||||
getRuntimeData() {
|
||||
return this._runtimeData;
|
||||
}
|
||||
|
||||
setRuntimeData(runtimeData: IRuntimeUnitDataType) {
|
||||
this._runtimeData = runtimeData;
|
||||
}
|
||||
|
||||
getArrayFormulaCellData() {
|
||||
return this._arrayFormulaCellData;
|
||||
}
|
||||
|
||||
setArrayFormulaCellData(unitData: IRuntimeUnitDataType) {
|
||||
this._arrayFormulaCellData = unitData;
|
||||
}
|
||||
|
||||
getArrayFormulaRange() {
|
||||
return this._arrayFormulaRange;
|
||||
}
|
||||
|
||||
setArrayFormulaRange(rangeData: IArrayFormulaRangeType) {
|
||||
this._arrayFormulaRange = rangeData;
|
||||
}
|
||||
|
||||
getRuntimeArrayFormulaCellData() {
|
||||
return this._runtimeArrayFormulaCellData;
|
||||
}
|
||||
|
||||
setRuntimeArrayFormulaCellData(unitData: IRuntimeUnitDataType) {
|
||||
this._runtimeArrayFormulaCellData = unitData;
|
||||
}
|
||||
|
||||
getRuntimeArrayFormulaRange() {
|
||||
return this._runtimeArrayFormulaRange;
|
||||
}
|
||||
|
||||
setRuntimeArrayFormulaRange(rangeData: IArrayFormulaRangeType) {
|
||||
this._runtimeArrayFormulaRange = rangeData;
|
||||
}
|
||||
|
||||
getRuntimeFeatureCellData() {
|
||||
return this._runtimeFeatureCellData;
|
||||
}
|
||||
|
||||
setRuntimeFeatureCellData(unitData: { [featureId: string]: IRuntimeUnitDataType }) {
|
||||
this._runtimeFeatureCellData = unitData;
|
||||
}
|
||||
|
||||
getActiveSheetRowCount() {
|
||||
return this.getCurrentActiveSheetData()?.rowCount || 0;
|
||||
}
|
||||
|
||||
getActiveSheetColumnCount() {
|
||||
return this.getCurrentActiveSheetData()?.columnCount || 0;
|
||||
}
|
||||
|
||||
getRowCount() {
|
||||
const rangeData = this.getRangeData();
|
||||
return rangeData.endRow - rangeData.startRow + 1;
|
||||
}
|
||||
|
||||
getColumnCount() {
|
||||
const rangeData = this.getRangeData();
|
||||
return rangeData.endColumn - rangeData.startColumn + 1;
|
||||
}
|
||||
|
||||
getRowData() {
|
||||
return this.getCurrentActiveSheetData()?.rowData || {};
|
||||
}
|
||||
|
||||
getColumnData() {
|
||||
return this.getCurrentActiveSheetData()?.columnData || {};
|
||||
}
|
||||
|
||||
isCell() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isColumn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isRow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isRange() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isTable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isCurrentRowForRange(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isCurrentColumnForRange(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMultiArea(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
unionBy(referenceObject: BaseReferenceObject): NodeValueType {
|
||||
/** abstract */
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
unionRange(rangeData1: IRange, rangeData2: IRange): IRange {
|
||||
/** abstract */
|
||||
return {
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
};
|
||||
}
|
||||
|
||||
getCellValueObject(cell: ICellData) {
|
||||
const value = getCellValue(cell);
|
||||
const cellValueType = Number(cell.t);
|
||||
if (ERROR_TYPE_SET.has(value as ErrorType)) {
|
||||
return ErrorValueObject.create(value as ErrorType);
|
||||
}
|
||||
|
||||
if (cellValueType === CellValueType.NUMBER) {
|
||||
const pattern = this._getPatternByCell(cell);
|
||||
|
||||
if (isTextFormat(pattern)) {
|
||||
return StringValueObject.create(value.toString()).withDateSystem(this._dateSystem);
|
||||
}
|
||||
|
||||
return createNumberValueObjectByRawValue(value, pattern).withDateSystem(this._dateSystem);
|
||||
}
|
||||
if (cellValueType === CellValueType.STRING || cellValueType === CellValueType.FORCE_STRING) {
|
||||
// A1 is `"test"`, =A1 also needs to get `"test"`
|
||||
return StringValueObject.create(value.toString()).withDateSystem(this._dateSystem);
|
||||
}
|
||||
if (cellValueType === CellValueType.BOOLEAN) {
|
||||
return createBooleanValueObjectByRawValue(value);
|
||||
}
|
||||
|
||||
return ValueObjectFactory.create(value, false, this._dateSystem);
|
||||
}
|
||||
|
||||
private _getPatternByCell(cell: ICellData) {
|
||||
const styles = this._unitStylesData[this.getUnitId()];
|
||||
|
||||
if (!styles) return '';
|
||||
|
||||
const style = styles.getStyleByCell(cell);
|
||||
return style?.n?.pattern || '';
|
||||
}
|
||||
|
||||
getCellByRow(row: number) {
|
||||
return this.getCellByPosition(row);
|
||||
}
|
||||
|
||||
getCellByColumn(column: number) {
|
||||
return this.getCellByPosition(undefined, column);
|
||||
}
|
||||
|
||||
getCurrentActiveSheetData() {
|
||||
return this._unitData[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentStylesData() {
|
||||
return this._unitStylesData[this.getUnitId()];
|
||||
}
|
||||
|
||||
getCurrentRuntimeSheetData() {
|
||||
return this._runtimeData?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentActiveArrayFormulaCellData() {
|
||||
return this._arrayFormulaCellData?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentRuntimeActiveArrayFormulaCellData() {
|
||||
return this._runtimeArrayFormulaCellData?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentActiveArrayFormulaRange() {
|
||||
return this._arrayFormulaRange?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentRuntimeActiveArrayFormulaRange() {
|
||||
return this._runtimeArrayFormulaRange?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCellData(row: number, column: number) {
|
||||
const activeSheetData = this.getCurrentActiveSheetData();
|
||||
|
||||
const activeRuntimeData = this.getCurrentRuntimeSheetData();
|
||||
|
||||
const activeArrayFormulaCellData = this.getCurrentActiveArrayFormulaCellData();
|
||||
|
||||
const activeRuntimeArrayFormulaCellData = this.getCurrentRuntimeActiveArrayFormulaCellData();
|
||||
|
||||
const activeArrayFormulaRangeMatrix = new ObjectMatrix<IRange>(this.getCurrentActiveArrayFormulaRange());
|
||||
|
||||
const activeRuntimeArrayFormulaRangeMatrix = new ObjectMatrix<IRange>(this.getCurrentRuntimeActiveArrayFormulaRange());
|
||||
|
||||
let cellData: Nullable<ICellData> = activeRuntimeData?.getValue(row, column) ||
|
||||
activeRuntimeArrayFormulaCellData?.getValue(row, column) ||
|
||||
this.getRuntimeFeatureCellValue(row, column);
|
||||
|
||||
if (cellData) {
|
||||
return cellData;
|
||||
}
|
||||
|
||||
cellData = activeArrayFormulaCellData?.getValue(row, column);
|
||||
|
||||
if (cellData) {
|
||||
// Check if the cell is in the runtime array formula range. if so, do not return the original array formula cell data.
|
||||
let isInRuntimeArrayFormulaRange = false;
|
||||
|
||||
activeArrayFormulaRangeMatrix.forValue((_row, _column, range) => {
|
||||
if (row >= range.startRow && row <= range.endRow && column >= range.startColumn && column <= range.endColumn && activeRuntimeArrayFormulaRangeMatrix.getValue(_row, _column)) {
|
||||
isInRuntimeArrayFormulaRange = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isInRuntimeArrayFormulaRange) {
|
||||
return cellData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return activeSheetData?.cellData.getValue(row, column);
|
||||
}
|
||||
|
||||
getRuntimeFeatureCellValue(row: number, column: number) {
|
||||
return getRuntimeFeatureCell(row, column, this.getSheetId(), this.getUnitId(), this._runtimeFeatureCellData);
|
||||
}
|
||||
|
||||
getCellByPosition(rowRaw?: number, columnRaw?: number) {
|
||||
const range = this.getRangePosition();
|
||||
const row = rowRaw ?? range.startRow;
|
||||
const column = columnRaw ?? range.startColumn;
|
||||
|
||||
const cell = this.getCellData(row, column);
|
||||
|
||||
if (!cell) {
|
||||
return NullValueObject.create();
|
||||
}
|
||||
|
||||
return this.getCellValueObject(cell);
|
||||
}
|
||||
|
||||
setCurrentRowAndColumn(row: number, column: number) {
|
||||
this._currentRow = row;
|
||||
this._currentColumn = column;
|
||||
}
|
||||
|
||||
getCurrentRow() {
|
||||
return this._currentRow;
|
||||
}
|
||||
|
||||
getCurrentColumn() {
|
||||
return this._currentColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pattern of the cell
|
||||
* @param unitId
|
||||
* @param sheetId
|
||||
* @param row
|
||||
* @param column
|
||||
* @returns
|
||||
*/
|
||||
getCellPattern(unitId: string, sheetId: string, row: number, column: number) {
|
||||
const currentStyles = this._unitStylesData[unitId];
|
||||
|
||||
if (!currentStyles) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const currentCell = this._unitData[unitId]?.[sheetId]?.cellData?.getValue(row, column);
|
||||
|
||||
if (!currentCell) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const style = currentStyles.getStyleByCell(currentCell);
|
||||
|
||||
return style?.n?.pattern || '';
|
||||
}
|
||||
|
||||
toArrayValueObject(useCache: boolean = true): ArrayValueObject {
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
|
||||
// A cached range can be reused only when its string and formatted-number interpretation matches.
|
||||
const key = `${this.getUnitId()}_${this.getSheetId()}_${startRow}_${endRow}_${startColumn}_${endColumn}_${this._dateSystem}`;
|
||||
|
||||
const array = FORMULA_REF_TO_ARRAY_CACHE.get(key);
|
||||
|
||||
if (array && useCache) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const rowSize = endRow - startRow + 1;
|
||||
const columnSize = endColumn - startColumn + 1;
|
||||
|
||||
if (rowSize < 0 || columnSize < 0) {
|
||||
return this._getBlankArrayValueObject();
|
||||
}
|
||||
|
||||
const arrayValueList: BaseValueObject[][] = new Array(rowSize);
|
||||
this.iterator((valueObject: Nullable<BaseValueObject>, rowIndex: number, columnIndex: number) => {
|
||||
const row = rowIndex - startRow;
|
||||
const column = columnIndex - startColumn;
|
||||
if (!arrayValueList[row]) {
|
||||
arrayValueList[row] = new Array(columnSize);
|
||||
}
|
||||
|
||||
if (valueObject == null) {
|
||||
valueObject = NullValueObject.create();
|
||||
}
|
||||
|
||||
arrayValueList[row][column] = valueObject;
|
||||
});
|
||||
|
||||
const arrayValueObjectData: IArrayValueObject = {
|
||||
calculateValueList: arrayValueList,
|
||||
rowCount: arrayValueList.length,
|
||||
columnCount: arrayValueList[0]?.length || 0,
|
||||
unitId: this.getUnitId(),
|
||||
sheetId: this.getSheetId(),
|
||||
row: startRow,
|
||||
column: startColumn,
|
||||
useInvertedIndexCache: true,
|
||||
};
|
||||
|
||||
const arrayValueObject = ArrayValueObject.create(arrayValueObjectData, this._dateSystem);
|
||||
|
||||
useCache && FORMULA_REF_TO_ARRAY_CACHE.set(key, arrayValueObject);
|
||||
|
||||
return arrayValueObject;
|
||||
}
|
||||
|
||||
toUnitRange() {
|
||||
return {
|
||||
range: this.getRangePosition(),
|
||||
sheetId: this.getSheetId(),
|
||||
unitId: this.getUnitId(),
|
||||
};
|
||||
}
|
||||
|
||||
toUnitRanges(): IUnitRange[] {
|
||||
return [this.toUnitRange()];
|
||||
}
|
||||
|
||||
private _checkIfWorksheetMiss() {
|
||||
if ((this._forcedSheetId == null || this._forcedSheetId.length === 0) && this._forcedSheetName.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getBlankArrayValueObject() {
|
||||
const arrayValueObjectData: IArrayValueObject = {
|
||||
calculateValueList: [],
|
||||
rowCount: 0,
|
||||
columnCount: 0,
|
||||
unitId: this.getUnitId(),
|
||||
sheetId: this.getSheetId(),
|
||||
row: 0,
|
||||
column: 0,
|
||||
};
|
||||
|
||||
return ArrayValueObject.create(arrayValueObjectData, this._dateSystem);
|
||||
}
|
||||
}
|
||||
|
||||
function isTypedEmptyStringCell(cell: Nullable<ICellData>): boolean {
|
||||
return cell?.v === '' && (cell.t === CellValueType.STRING || cell.t === CellValueType.FORCE_STRING);
|
||||
}
|
||||
|
||||
export class AsyncObject extends ObjectClassType {
|
||||
constructor(private _promise: Promise<BaseValueObject>) {
|
||||
super();
|
||||
}
|
||||
|
||||
override isAsyncObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getValue() {
|
||||
return this._promise;
|
||||
}
|
||||
}
|
||||
|
||||
export class AsyncArrayObject extends ObjectClassType {
|
||||
constructor(private _promiseList: Array<Array<BaseValueObject | AsyncObject>>) {
|
||||
super();
|
||||
}
|
||||
|
||||
override isAsyncArrayObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getValue() {
|
||||
const variants: BaseValueObject[][] = [];
|
||||
|
||||
for (let r = 0; r < this._promiseList.length; r++) {
|
||||
const promiseCells = this._promiseList[r];
|
||||
if (variants[r] == null) {
|
||||
variants[r] = [];
|
||||
}
|
||||
for (let c = 0; c < promiseCells.length; c++) {
|
||||
const promiseCell = promiseCells[c];
|
||||
if ((promiseCell as AsyncObject).isAsyncObject()) {
|
||||
variants[r][c] = await (promiseCell as AsyncObject).getValue();
|
||||
} else {
|
||||
variants[r][c] = promiseCell as BaseValueObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const arrayValueObjectData: IArrayValueObject = {
|
||||
calculateValueList: variants,
|
||||
rowCount: variants.length,
|
||||
columnCount: variants[0]?.length || 0,
|
||||
unitId: '',
|
||||
sheetId: '',
|
||||
row: 0,
|
||||
column: 0,
|
||||
};
|
||||
|
||||
return ArrayValueObject.create(arrayValueObjectData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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 { IRange } from '@univerjs/core';
|
||||
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
|
||||
import { deserializeRangeWithSheetWithCache } from '../utils/reference-cache';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
import { RangeReferenceObject } from './range-reference-object';
|
||||
|
||||
const EXCEL_MAX_ROWS = 1_048_576;
|
||||
const EXCEL_MAX_COLUMNS = 16_384;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
override isCell() {
|
||||
return true;
|
||||
}
|
||||
|
||||
override isExceedRange() {
|
||||
if ((this.getForcedSheetId() == null || this.getForcedSheetId()?.length === 0) && this.getForcedSheetName().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
return startRow < 0 || startColumn < 0 || endRow >= EXCEL_MAX_ROWS || endColumn >= EXCEL_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
override unionBy(referenceObject: BaseReferenceObject) {
|
||||
if (!referenceObject.isCell()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const cellReferenceObject = referenceObject as CellReferenceObject;
|
||||
// if (cellReferenceObject.getForcedSheetName() !== undefined) {
|
||||
// return ErrorValueObject.create(ErrorType.REF);
|
||||
// }
|
||||
|
||||
const newRangeData = this.unionRange(this.getRangePosition(), cellReferenceObject.getRangePosition());
|
||||
|
||||
return this._createRange(newRangeData);
|
||||
}
|
||||
|
||||
override unionRange(rangeData1: IRange, rangeData2: IRange) {
|
||||
const startRow1 = rangeData1.startRow;
|
||||
const startColumn1 = rangeData1.startColumn;
|
||||
|
||||
const startRow2 = rangeData2.startRow;
|
||||
const startColumn2 = rangeData2.startColumn;
|
||||
const range: IRange = {
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
};
|
||||
if (startRow1 > startRow2) {
|
||||
range.startRow = startRow2;
|
||||
range.endRow = startRow1;
|
||||
} else {
|
||||
range.startRow = startRow1;
|
||||
range.endRow = startRow2;
|
||||
}
|
||||
|
||||
if (startColumn1 > startColumn2) {
|
||||
range.startColumn = startColumn2;
|
||||
range.endColumn = startColumn1;
|
||||
} else {
|
||||
range.startColumn = startColumn1;
|
||||
range.endColumn = startColumn2;
|
||||
}
|
||||
|
||||
if (rangeData1.startAbsoluteRefType) {
|
||||
range.startAbsoluteRefType = rangeData1.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
if (rangeData2.startAbsoluteRefType) {
|
||||
range.endAbsoluteRefType = rangeData2.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private _createRange(newRangeData: IRange) {
|
||||
const rangeReferenceObject = new RangeReferenceObject(
|
||||
newRangeData,
|
||||
this.getForcedSheetId(),
|
||||
this.getForcedUnitId()
|
||||
);
|
||||
|
||||
rangeReferenceObject.setUnitData(this.getUnitData());
|
||||
|
||||
rangeReferenceObject.setDefaultSheetId(this.getDefaultSheetId());
|
||||
|
||||
rangeReferenceObject.setDefaultUnitId(this.getDefaultUnitId());
|
||||
|
||||
rangeReferenceObject.setRuntimeData(this.getRuntimeData());
|
||||
|
||||
rangeReferenceObject.setUnitStylesData(this.getUnitStylesData());
|
||||
|
||||
rangeReferenceObject.setArrayFormulaCellData(this.getArrayFormulaCellData());
|
||||
|
||||
rangeReferenceObject.setRuntimeArrayFormulaCellData(this.getRuntimeArrayFormulaCellData());
|
||||
|
||||
rangeReferenceObject.setRuntimeFeatureCellData(this.getRuntimeFeatureCellData());
|
||||
|
||||
const forceSheetId = this.getForcedSheetId();
|
||||
|
||||
rangeReferenceObject.setForcedSheetName(this.getForcedSheetName());
|
||||
|
||||
if (forceSheetId != null) {
|
||||
rangeReferenceObject.setForcedSheetIdDirect(forceSheetId);
|
||||
}
|
||||
|
||||
// const forcedSheetID = this.getForcedSheetId();
|
||||
// if (forcedSheetID) {
|
||||
// rangeReferenceObject.setForcedSheetIdDirect(forcedSheetID);
|
||||
// }
|
||||
|
||||
const forcedUnitId = this.getForcedUnitId();
|
||||
if (forcedUnitId) {
|
||||
rangeReferenceObject.setForcedUnitIdDirect(forcedUnitId);
|
||||
}
|
||||
|
||||
return rangeReferenceObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { IRange } from '@univerjs/core';
|
||||
import { RANGE_TYPE } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { matchToken } from '../../basics/token';
|
||||
import { deserializeRangeWithSheetWithCache } from '../utils/reference-cache';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
|
||||
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 = {
|
||||
...grid.range,
|
||||
startColumn: grid.range.startColumn,
|
||||
startRow: Number.NaN,
|
||||
endColumn: grid.range.endColumn,
|
||||
endRow: Number.NaN,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
};
|
||||
this.setRangeData(range);
|
||||
}
|
||||
|
||||
override isColumn() {
|
||||
return true;
|
||||
}
|
||||
|
||||
override unionBy(referenceObject: BaseReferenceObject) {
|
||||
if (!referenceObject.isColumn()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const columnReferenceObject = referenceObject as ColumnReferenceObject;
|
||||
if (
|
||||
columnReferenceObject.getForcedSheetName() !== undefined &&
|
||||
columnReferenceObject.getForcedSheetName() !== ''
|
||||
) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const currentRangeData = this.getRangeData();
|
||||
|
||||
// if (currentRangeData.endColumn !== -1) {
|
||||
// return ErrorValueObject.create(ErrorType.REF);
|
||||
// }
|
||||
|
||||
const newColumnRange = columnReferenceObject.getRangeData();
|
||||
|
||||
const newColumn = newColumnRange.startColumn;
|
||||
|
||||
// if the column is already in the range, return the same object
|
||||
if (newColumn >= currentRangeData.startColumn && newColumn <= currentRangeData.endColumn) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const column = currentRangeData.startColumn;
|
||||
|
||||
if (newColumn > column) {
|
||||
currentRangeData.endColumn = newColumn;
|
||||
} else {
|
||||
currentRangeData.startColumn = newColumn;
|
||||
currentRangeData.endColumn = column;
|
||||
}
|
||||
|
||||
if (newColumnRange.startAbsoluteRefType) {
|
||||
currentRangeData.endAbsoluteRefType = newColumnRange.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
currentRangeData.rangeType = RANGE_TYPE.COLUMN;
|
||||
|
||||
this.setToken(`${this.getToken()}${matchToken.COLON}${columnReferenceObject.getToken()}`);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user