Skip to content

Commit

Permalink
fix(formula): fix formula rows and columns limit (#4052)
Browse files Browse the repository at this point in the history
Co-authored-by: wpxp123456 <Wpxp1223456>
  • Loading branch information
wpxp123456 authored Nov 13, 2024
1 parent 17bbe40 commit 9a78db0
Show file tree
Hide file tree
Showing 7 changed files with 486 additions and 175 deletions.
6 changes: 6 additions & 0 deletions packages/engine-formula/src/engine/ast-node/function-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ export class FunctionNode extends BaseAstNode {
const { currentUnitId, currentSubUnitId, currentRow, currentColumn } = this._runtimeService;

this._functionExecutor.setRefInfo(currentUnitId, currentSubUnitId, currentRow, currentColumn);

if (this._functionExecutor.needsSheetRowColumnCount) {
const { rowCount, columnCount } = this._currentConfigService.getSheetRowColumnCount(currentUnitId, currentSubUnitId);

this._functionExecutor.setSheetRowColumnCount(rowCount, columnCount);
}
}

private _setRefData(variant: FunctionVariantType) {
Expand Down
12 changes: 12 additions & 0 deletions packages/engine-formula/src/functions/base-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class BaseFunction {
private _sheetOrder: string[];
private _sheetNameMap: { [sheetId: string]: string };
protected _formulaDataModel: Nullable<FormulaDataModel>;
protected _rowCount: number = -1;
protected _columnCount: number = -1;

/**
* Whether the function needs to expand the parameters
Expand All @@ -71,6 +73,11 @@ export class BaseFunction {
*/
needsFormulaDataModel: boolean = false;

/**
* Whether the function needs the number of rows and columns in the sheet
*/
needsSheetRowColumnCount: boolean = false;

/**
* Minimum number of parameters
*/
Expand Down Expand Up @@ -159,6 +166,11 @@ export class BaseFunction {
this._formulaDataModel = _formulaDataModel;
}

setSheetRowColumnCount(rowCount: number, columnCount: number) {
this._rowCount = rowCount;
this._columnCount = columnCount;
}

isAsync() {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,109 +14,251 @@
* limitations under the License.
*/

import { describe, expect, it, vi } from 'vitest';
import type { Injector, IWorkbookData } from '@univerjs/core';
import type { LexerNode } from '../../../../engine/analysis/lexer-node';

import type { BaseAstNode } from '../../../../engine/ast-node/base-ast-node';
import { CellValueType, LocaleType } from '@univerjs/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { Lexer } from '../../../../engine/analysis/lexer';
import { AstTreeBuilder } from '../../../../engine/analysis/parser';
import { Interpreter } from '../../../../engine/interpreter/interpreter';
import { generateExecuteAstNodeData } from '../../../../engine/utils/ast-node-tool';
import { IFormulaCurrentConfigService } from '../../../../services/current-data.service';
import { IFunctionService } from '../../../../services/function.service';
import { IFormulaRuntimeService } from '../../../../services/runtime.service';
import { createFunctionTestBed, getObjectValue } from '../../../__tests__/create-function-test-bed';
import { FUNCTION_NAMES_MATH } from '../../function-names';
import { Randarray } from '../index';
import { BooleanValueObject, NullValueObject, NumberValueObject } from '../../../../engine/value-object/primitive-object';
import { ArrayValueObject, transformToValue } from '../../../../engine/value-object/array-value-object';
import { ErrorType } from '../../../../basics/error-type';

const getTestWorkbookData = (): IWorkbookData => {
return {
id: 'test',
appVersion: '3.0.0-alpha',
sheets: {
sheet1: {
id: 'sheet1',
cellData: {
0: {
0: {
v: 1,
t: CellValueType.NUMBER,
},
1: {
v: true,
t: CellValueType.BOOLEAN,
},
2: {
v: false,
t: CellValueType.BOOLEAN,
},
3: {
v: 'test',
t: CellValueType.STRING,
},
4: {
v: null,
},
5: {
v: ErrorType.NAME,
t: CellValueType.STRING,
},
},
1: {
0: {
v: 1,
t: CellValueType.NUMBER,
},
1: {
v: 2,
t: CellValueType.NUMBER,
},
2: {
v: 1,
t: CellValueType.NUMBER,
},
3: {
v: 2.5,
t: CellValueType.NUMBER,
},
},
2: {
0: {
v: 2,
t: CellValueType.NUMBER,
},
1: {
v: 3,
t: CellValueType.NUMBER,
},
2: {
v: 2,
t: CellValueType.NUMBER,
},
3: {
v: 3,
t: CellValueType.NUMBER,
},
},
3: {
0: {
v: null,
},
1: {
v: 1,
t: CellValueType.NUMBER,
},
},
4: {
0: {
v: null,
},
1: {
v: null,
},
},
},
rowCount: 1000,
columnCount: 20,
},
},
locale: LocaleType.ZH_CN,
name: '',
sheetOrder: [],
styles: {},
};
};

describe('Test randarray function', () => {
const testFunction = new Randarray(FUNCTION_NAMES_MATH.RANDARRAY);
let get: Injector['get'];
let lexer: Lexer;
let astTreeBuilder: AstTreeBuilder;
let interpreter: Interpreter;
let calculate: (formula: string) => (string | number | boolean | null)[][] | string | number | boolean;

beforeEach(() => {
const testBed = createFunctionTestBed(getTestWorkbookData());

get = testBed.get;

lexer = get(Lexer);
astTreeBuilder = get(AstTreeBuilder);
interpreter = get(Interpreter);

const functionService = get(IFunctionService);

const formulaCurrentConfigService = get(IFormulaCurrentConfigService);

const formulaRuntimeService = get(IFormulaRuntimeService);

formulaCurrentConfigService.load({
formulaData: {},
arrayFormulaCellData: {},
arrayFormulaRange: {},
forceCalculate: false,
dirtyRanges: [],
dirtyNameMap: {},
dirtyDefinedNameMap: {},
dirtyUnitFeatureMap: {},
dirtyUnitOtherFormulaMap: {},
excludedCell: {},
allUnitData: {
[testBed.unitId]: testBed.sheetData,
},
});

const sheetItem = testBed.sheetData[testBed.sheetId];

formulaRuntimeService.setCurrent(
0,
0,
sheetItem.rowCount,
sheetItem.columnCount,
testBed.sheetId,
testBed.unitId
);

functionService.registerExecutors(
new Randarray(FUNCTION_NAMES_MATH.RANDARRAY)
);

calculate = (formula: string) => {
const lexerNode = lexer.treeBuilder(formula);

const astNode = astTreeBuilder.parse(lexerNode as LexerNode);

const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));

return getObjectValue(result);
};
});

describe('Randarray', () => {
const mockRandom = vi.spyOn(Math, 'random');
mockRandom.mockReturnValue(0.5);

it('function is normal', () => {
const result = testFunction.calculate();
expect(result.getValue()).toBe(0.5);
it('Value is normal', async () => {
const result = await calculate('=RANDARRAY()');
expect(result).toBe(0.5);
});

it('rows is number', () => {
const rows = NumberValueObject.create(3);
const result = testFunction.calculate(rows);
expect(transformToValue(result.getArrayValue())).toStrictEqual([
it('rows is number', async () => {
const result = await calculate('=RANDARRAY(3)');
expect(result).toStrictEqual([
[0.5],
[0.5],
[0.5],
]);
});

it('rows is number, columns is number', () => {
const rows = NumberValueObject.create(3);
const columns = NumberValueObject.create(3);
const result = testFunction.calculate(rows, columns);
expect(transformToValue(result.getArrayValue())).toStrictEqual([
it('rows is number, columns is number', async () => {
const result = await calculate('=RANDARRAY(3,3)');
expect(result).toStrictEqual([
[0.5, 0.5, 0.5],
[0.5, 0.5, 0.5],
[0.5, 0.5, 0.5],
]);
});

it('rows or columns is 0/-2/false/blank cell', () => {
const rows = NumberValueObject.create(0);
const result = testFunction.calculate(rows);
expect(result.getValue()).toBe(ErrorType.CALC);
it('rows and columns is exceed', async () => {
const result = await calculate('=RANDARRAY(10000,10000)');
expect(result).toBe(ErrorType.VALUE);

const result2 = await calculate('=RANDARRAY(2000,3)');
expect(result2).toBe(ErrorType.REF);
});

const rows2 = NumberValueObject.create(-2);
const result2 = testFunction.calculate(rows2);
expect(result2.getValue()).toBe(ErrorType.VALUE);
it('rows or columns is 0/-2/false/blank cell', async () => {
const result = await calculate('=RANDARRAY(0)');
expect(result).toBe(ErrorType.CALC);

const rows3 = BooleanValueObject.create(false);
const result3 = testFunction.calculate(rows3);
expect(result3.getValue()).toBe(ErrorType.CALC);
const result2 = await calculate('=RANDARRAY(-2)');
expect(result2).toBe(ErrorType.VALUE);

const rows4 = NullValueObject.create();
const result4 = testFunction.calculate(rows4);
expect(result4.getValue()).toBe(ErrorType.CALC);
const result3 = await calculate('=RANDARRAY(C1)');
expect(result3).toBe(ErrorType.CALC);

const result4 = await calculate('=RANDARRAY(E1)');
expect(result4).toBe(ErrorType.CALC);
});

it('Value is array', () => {
const rows = NumberValueObject.create(6);
const columns = NumberValueObject.create(6);
const min = ArrayValueObject.createByArray([
[1],
[2],
]);
const max = ArrayValueObject.createByArray([
[1, 2],
[2, 3],
]);
const result = testFunction.calculate(rows, columns, min, max);
expect(transformToValue(result.getArrayValue())).toStrictEqual([
it('Value is array', async () => {
const result = await calculate('=RANDARRAY(6,6,A2:A3,A2:B3)');
expect(result).toStrictEqual([
[1, 1.5],
[2, 2.5],
]);
});

it('min > max', () => {
const rows = NumberValueObject.create(3);
const columns = NumberValueObject.create(3);
const min = NumberValueObject.create(3);
const max = NumberValueObject.create(2);
const result = testFunction.calculate(rows, columns, min, max);
expect(result.getValue()).toBe(ErrorType.VALUE);
it('min > max', async () => {
const result = await calculate('=RANDARRAY(3,3,3,2)');
expect(result).toBe(ErrorType.VALUE);
});

it('wholeNumber', () => {
const rows = NumberValueObject.create(6);
const columns = NumberValueObject.create(6);
const min = ArrayValueObject.createByArray([
[1],
[2],
]);
const max = ArrayValueObject.createByArray([
[1, 2.5],
[2, 3],
]);
const wholeNumber = ArrayValueObject.createByArray([
[null, 1],
[null, null],
]);
const result = testFunction.calculate(rows, columns, min, max, wholeNumber);
expect(transformToValue(result.getArrayValue())).toStrictEqual([
it('wholeNumber', async () => {
const result = await calculate('=RANDARRAY(6,6,C2:C3,C2:D3,A4:B5)');
expect(result).toStrictEqual([
[1, ErrorType.VALUE],
[2, 2.5],
]);
Expand Down
Loading

0 comments on commit 9a78db0

Please sign in to comment.