Skip to content

Commit

Permalink
fix(formula): index function supports reference object (#1657)
Browse files Browse the repository at this point in the history
* fix(formula): covert serial number to date in concatenate function

* fix(formula): index function supports reference object

* fix(formula): offset function returns cell reference

* fix(formula): remove row count limit, update formula ref range
  • Loading branch information
Dushusir committed Mar 23, 2024
1 parent a875a31 commit b47487a
Show file tree
Hide file tree
Showing 20 changed files with 521 additions and 101 deletions.
2 changes: 1 addition & 1 deletion examples/src/sheets/main.ts
Expand Up @@ -31,8 +31,8 @@ import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor';
import { UniverUIPlugin } from '@univerjs/ui';

import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data';
import { DebuggerPlugin } from '../plugins/debugger';
import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo';
import { locales } from './locales';

const LOAD_LAZY_PLUGINS_TIMEOUT = 1_000;
Expand Down
3 changes: 2 additions & 1 deletion packages/engine-formula/package.json
Expand Up @@ -66,7 +66,8 @@
"rxjs": ">=7.0.0"
},
"dependencies": {
"big.js": "^6.2.1"
"big.js": "^6.2.1",
"numfmt": "^2.5.2"
},
"devDependencies": {
"@types/big.js": "^6.2.2",
Expand Down
30 changes: 30 additions & 0 deletions packages/engine-formula/src/basics/format.ts
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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.
*/

// @ts-ignore
import numfmt from 'numfmt';

/**
* covert number to preview string by pattern
* @TODODushusir: Internationalization, reuse with numfmt
*
* @param pattern
* @param value
* @returns
*/
export const getFormatPreview = (pattern: string, value: number) => {
return numfmt.format(pattern, value);
};
Expand Up @@ -230,7 +230,7 @@ describe('Test Reference', () => {
const testTrueCases = [
'sheet-1',
'sheet 1',
'B1048576',
'B1048577',
'RC',
'RC2',
'R5C',
Expand All @@ -247,7 +247,7 @@ describe('Test Reference', () => {
'!Sheet',
'!Sheet',
];
const testFalseCase = ['Sheet1', '工作表1', 'B1048577'];
const testFalseCase = ['Sheet1', '工作表1'];

testTrueCases.forEach((testTrueCase) => {
expect(needsQuoting(testTrueCase)).toBeTruthy();
Expand Down
5 changes: 3 additions & 2 deletions packages/engine-formula/src/engine/utils/reference.ts
Expand Up @@ -291,7 +291,7 @@ export function deserializeRangeWithSheet(refString: string): IUnitRangeName {
* Determine whether the sheet name needs to be wrapped in quotes
* Excel will quote the worksheet name if any of the following is true:
* - It contains any space or punctuation characters, such as ()$,;-{}"'()【】“”‘’%… and many more
* - It is a valid cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
* - It is a valid cell reference in A1 notation, e.g. B1048576 is quoted
* - It is a valid cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
* - It starts with a non-letter, e.g. 99, 1.5, 12a, 💩a
* - Excel will not quote worksheet names if they only contain non-punctuation, non-letter characters in non-initial positions. For example, a💩 remains unquoted.*
Expand Down Expand Up @@ -332,7 +332,8 @@ export function needsQuoting(name: string) {

function isA1Notation(name: string) {
const match = name.match(/[1-9][0-9]{0,6}/);
return /^[A-Z]+[1-9][0-9]{0,6}$/.test(name) && match !== null && Number.parseInt(match[0], 10) <= 1048576;
// Excel has a limit on the number of rows and columns: targetRow > 1048576 || targetColumn > 16384, Univer has no limit
return /^[A-Z]+[1-9][0-9]{0,6}$/.test(name) && match !== null;
}

function isR1C1Notation(name: string) {
Expand Down
53 changes: 51 additions & 2 deletions packages/engine-formula/src/functions/base-function.ts
Expand Up @@ -14,20 +14,26 @@
* limitations under the License.
*/

import type { Nullable } from '@univerjs/core';
import type { IRange, Nullable } from '@univerjs/core';
import { Disposable } from '@univerjs/core';

import { ErrorType } from '../basics/error-type';
import type { IFunctionNames } from '../basics/function';
import { compareToken } from '../basics/token';
import type { FunctionVariantType, NodeValueType } from '../engine/reference-object/base-reference-object';
import type { BaseReferenceObject, FunctionVariantType, NodeValueType } from '../engine/reference-object/base-reference-object';
import type { ArrayBinarySearchType } from '../engine/utils/compare';
import { ArrayOrderSearchType } from '../engine/utils/compare';
import type { ArrayValueObject } from '../engine/value-object/array-value-object';
import { type BaseValueObject, ErrorValueObject } from '../engine/value-object/base-value-object';
import { NullValueObject, NumberValueObject, type PrimitiveValueType } from '../engine/value-object/primitive-object';
import { convertTonNumber } from '../engine/utils/object-covert';
import { createNewArray } from '../engine/utils/array-object';
import { serializeRangeToRefString } from '../engine/utils/reference';
import { REFERENCE_REGEX_SINGLE_COLUMN, REFERENCE_REGEX_SINGLE_ROW, REFERENCE_SINGLE_RANGE_REGEX } from '../basics/regex';
import { CellReferenceObject } from '../engine/reference-object/cell-reference-object';
import { RowReferenceObject } from '../engine/reference-object/row-reference-object';
import { ColumnReferenceObject } from '../engine/reference-object/column-reference-object';
import { RangeReferenceObject } from '../engine/reference-object/range-reference-object';

export class BaseFunction extends Disposable {
private _unitId: Nullable<string>;
Expand Down Expand Up @@ -450,4 +456,47 @@ export class BaseFunction extends Disposable {

return valueObject;
}

createReferenceObject(reference: BaseReferenceObject, range: IRange) {
const unitId = reference.getForcedUnitId();
const sheetId = reference.getForcedSheetId() || '';
const sheetName = reference.getForcedSheetName();

const gridRangeName = {
unitId,
sheetName,
range,
};

const token = serializeRangeToRefString(gridRangeName);

let referenceObject: BaseReferenceObject;

if (new RegExp(REFERENCE_SINGLE_RANGE_REGEX).test(token)) {
referenceObject = new CellReferenceObject(token);
} else if (new RegExp(REFERENCE_REGEX_SINGLE_ROW).test(token)) {
referenceObject = new RowReferenceObject(token);
} else if (new RegExp(REFERENCE_REGEX_SINGLE_COLUMN).test(token)) {
referenceObject = new ColumnReferenceObject(token);
} else {
referenceObject = new RangeReferenceObject(range, sheetId, unitId);
}

return this._setReferenceDefault(reference, referenceObject); ;
}

private _setReferenceDefault(reference: BaseReferenceObject, object: BaseReferenceObject) {
if (this.unitId == null || this.subUnitId == null) {
return ErrorValueObject.create(ErrorType.REF);
}

object.setDefaultUnitId(this.unitId);
object.setDefaultSheetId(this.subUnitId);
object.setUnitData(reference.getUnitData());
object.setRuntimeData(reference.getRuntimeData());
object.setArrayFormulaCellData(reference.getArrayFormulaCellData());
object.setRuntimeArrayFormulaCellData(reference.getRuntimeArrayFormulaCellData());

return object;
}
}
Expand Up @@ -33,6 +33,7 @@ import type { BaseValueObject, ErrorValueObject } from '../../../../engine/value
import type { ArrayValueObject } from '../../../../engine/value-object/array-value-object';
import { Index } from '..';
import { ErrorType } from '../../../../basics/error-type';
import type { BaseReferenceObject } from '../../../../engine/reference-object/base-reference-object';

const getTestWorkbookData = (): IWorkbookData => {
return {
Expand Down Expand Up @@ -201,6 +202,8 @@ describe('Test index', () => {

if ((result as ErrorValueObject).isError()) {
return (result as ErrorValueObject).getValue();
} else if ((result as BaseReferenceObject).isReferenceObject()) {
return (result as BaseReferenceObject).toArrayValueObject().toValue();
} else if ((result as ArrayValueObject).isArray()) {
return (result as ArrayValueObject).toValue();
}
Expand Down Expand Up @@ -544,6 +547,12 @@ describe('Test index', () => {

expect(result).toStrictEqual([['Tom', ErrorType.VALUE, 'Tom', 'Tom', 'Tom', 'Tom'], ['Tom', ErrorType.REF, ErrorType.REF, ErrorType.VALUE, ErrorType.VALUE, 'Tom'], [ErrorType.NA, ErrorType.NA, ErrorType.NA, ErrorType.NA, ErrorType.NA, ErrorType.NA]]);
});

it('The result of the INDEX function is a reference', async () => {
const result = await calculate('=INDEX(A2:A5,2,1):A1');

expect(result).toStrictEqual([[1], [3], [1]]);
});
});

describe('Multi rows and columns', () => {
Expand Down
93 changes: 73 additions & 20 deletions packages/engine-formula/src/functions/lookup/index/index.ts
Expand Up @@ -15,14 +15,27 @@
*/

import { ErrorType } from '../../../basics/error-type';
import type { BaseReferenceObject } from '../../../engine/reference-object/base-reference-object';
import { expandArrayValueObject } from '../../../engine/utils/array-object';
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import { type BaseValueObject, ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NullValueObject, NumberValueObject } from '../../../engine/value-object/primitive-object';
import { BaseFunction } from '../../base-function';

/**
* The result of the INDEX function is a reference and is interpreted as such by other formulas. Depending on the formula, the return value of INDEX may be used as a reference or as a value.
*
* =INDEX(A2:A5,2,1):A1 same as =A1:A3
*
* OPTIMIZE: maybe we can remove some unknown type
*/
export class Index extends BaseFunction {
override calculate(reference: BaseValueObject, rowNum: BaseValueObject, columnNum?: BaseValueObject, areaNum?: BaseValueObject) {
override needsReferenceObject = true;

override calculate(referenceObject: BaseValueObject, rowNum: BaseValueObject, columnNum?: BaseValueObject, areaNum?: BaseValueObject) {
// covert to real type
const reference = referenceObject as unknown as BaseReferenceObject;

if (reference == null) {
return ErrorValueObject.create(ErrorType.NA);
}
Expand All @@ -43,12 +56,13 @@ export class Index extends BaseFunction {
return areaNum;
}

if (!reference.isArray()) {
if (!(reference.isRange())) {
return ErrorValueObject.create(ErrorType.REF);
}

const referenceRowCount = (reference as ArrayValueObject).getRowCount();
const referenceColumnCount = (reference as ArrayValueObject).getColumnCount();
const { startRow, endRow, startColumn, endColumn } = reference.getRangeData();
const referenceRowCount = endRow - startRow + 1;
const referenceColumnCount = endColumn - startColumn + 1;

// When there is only one row, the rowNum is considered to be the column number.
// =INDEX(A6:B6,2) equals =INDEX(A6:B6,1,2)
Expand All @@ -62,6 +76,18 @@ export class Index extends BaseFunction {

areaNum = areaNum ?? NumberValueObject.create(1);

if (rowNum.isReferenceObject()) {
rowNum = (rowNum as unknown as BaseReferenceObject).toArrayValueObject();
}

if (columnNum.isReferenceObject()) {
columnNum = (columnNum as unknown as BaseReferenceObject).toArrayValueObject();
}

if (areaNum.isReferenceObject()) {
areaNum = (areaNum as unknown as BaseReferenceObject).toArrayValueObject();
}

// get max row length
const maxRowLength = Math.max(
rowNum.isArray() ? (rowNum as ArrayValueObject).getRowCount() : 1,
Expand All @@ -79,7 +105,7 @@ export class Index extends BaseFunction {
// If maxRowLength and maxColumnLength are both 1, pick the value from the reference array
// Otherwise, filter the results from the reference according to the specified rowNum/columnNum/areaNum, take the upper left corner cell value of each result array, and then form an array with maxRowLength row number and maxColumnLength column number.
if (maxRowLength === 1 && maxColumnLength === 1) {
return this._calculateSingleCell(reference as ArrayValueObject, rowNum, columnNum, areaNum);
return this._calculateSingleCell(reference, rowNum, columnNum, areaNum);
} else {
const rowNumArray = expandArrayValueObject(maxRowLength, maxColumnLength, rowNum, ErrorValueObject.create(ErrorType.NA));
const columnNumArray = expandArrayValueObject(maxRowLength, maxColumnLength, columnNum, ErrorValueObject.create(ErrorType.NA));
Expand All @@ -89,18 +115,18 @@ export class Index extends BaseFunction {
const columnNumValue = columnNumArray.get(rowIndex, columnIndex) || NullValueObject.create();
const areaNumValue = areaNumArray.get(rowIndex, columnIndex) || NullValueObject.create();

const result = this._calculateSingleCell(reference as ArrayValueObject, rowNumValue, columnNumValue, areaNumValue);
const result = this._calculateSingleCell(reference, rowNumValue, columnNumValue, areaNumValue);

if (result.isArray()) {
return (result as ArrayValueObject).getFirstCell();
if (result.isReferenceObject()) {
return (result as BaseReferenceObject).toArrayValueObject().getFirstCell();
}

return result;
return result as BaseValueObject;
});
}
}

private _calculateSingleCell(reference: ArrayValueObject, rowNum: BaseValueObject, columnNum: BaseValueObject, areaNum: BaseValueObject) {
private _calculateSingleCell(reference: BaseReferenceObject, rowNum: BaseValueObject, columnNum: BaseValueObject, areaNum: BaseValueObject) {
if (rowNum.isError()) {
return rowNum;
}
Expand Down Expand Up @@ -132,16 +158,7 @@ export class Index extends BaseFunction {
return ErrorValueObject.create(ErrorType.VALUE);
}

const rowParam = rowNumberValue === 0 ? [undefined] : [rowNumberValue - 1, rowNumberValue];
const columnParam = columnNumberValue === 0 ? [undefined] : [columnNumberValue - 1, columnNumberValue];

const result = reference.slice(rowParam, columnParam);

if (!result) {
return ErrorValueObject.create(ErrorType.REF);
}

return result;
return this._getReferenceObject(reference, rowNumberValue, columnNumberValue, areaNumberValue);
}

private _getNumberValue(numberValueObject?: BaseValueObject) {
Expand Down Expand Up @@ -189,4 +206,40 @@ export class Index extends BaseFunction {

return logicValue;
}

private _getReferenceObject(reference: BaseReferenceObject, rowNumberValue: number, columnNumberValue: number, areaNumberValue: number) {
const { startRow, endRow, startColumn, endColumn } = reference.getRangeData();

let referenceStartRow = 0;
let referenceEndRow = 0;
let referenceStartColumn = 0;
let referenceEndColumn = 0;

if (rowNumberValue === 0) {
referenceStartRow = startRow;
referenceEndRow = endRow;
} else {
referenceStartRow = referenceEndRow = startRow + rowNumberValue - 1;
}

if (columnNumberValue === 0) {
referenceStartColumn = startColumn;
referenceEndColumn = endColumn;
} else {
referenceStartColumn = referenceEndColumn = startColumn + columnNumberValue - 1;
}

if (referenceStartRow > endRow || referenceStartColumn > endColumn) {
return ErrorValueObject.create(ErrorType.REF);
}

const range = {
startRow: referenceStartRow,
startColumn: referenceStartColumn,
endRow: referenceEndRow,
endColumn: referenceEndColumn,
};

return this.createReferenceObject(reference, range);
}
}

0 comments on commit b47487a

Please sign in to comment.