Skip to content

Commit

Permalink
feat(formula): supplement text formulas (#3842)
Browse files Browse the repository at this point in the history
Co-authored-by: wpxp123456 <Wpxp1223456>
  • Loading branch information
wpxp123456 authored Nov 5, 2024
1 parent 6142b07 commit 114f6dc
Show file tree
Hide file tree
Showing 47 changed files with 3,535 additions and 716 deletions.
25 changes: 25 additions & 0 deletions packages/engine-formula/src/basics/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
* limitations under the License.
*/

import type { BaseValueObject } from '../engine/value-object/base-value-object';
import { numfmt } from '@univerjs/core';
import { stripErrorMargin } from '../engine/utils/math-kit';

/**
* covert number to preview string by pattern
Expand All @@ -27,3 +29,26 @@ import { numfmt } from '@univerjs/core';
export const getFormatPreview = (pattern: string, value: number) => {
return numfmt.format(pattern, value, { 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());
} 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;
};
12 changes: 0 additions & 12 deletions packages/engine-formula/src/engine/utils/char-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,6 @@ export function charLenByte(text: string): number {
let byteCount = 0;

for (let i = 0; i < text.length; i++) {
// const charCode = text.charCodeAt(i);

// if (
// (charCode >= 0x3040 && charCode <= 0x30FF) || // Japanese hiragana and katakana
// (charCode >= 0x4E00 && charCode <= 0x9FFF) || // Chinese (simplified and traditional)
// (charCode >= 0xAC00 && charCode <= 0xD7AF) // Korean language
// ) {
// byteCount += 2;
// } else {
// byteCount += 1;
// }

byteCount += getCharLenByteInText(text, i);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ describe('Test nested functions', () => {

it('Len gets length from formula result', () => {
let result = calculate('=LEN(1/3)');
expect(result).toStrictEqual(17);
expect(result).toStrictEqual(14);

result = calculate('=LEN(0.1+0.2)');
expect(result).toStrictEqual(3);
Expand Down
143 changes: 143 additions & 0 deletions packages/engine-formula/src/functions/text/find/__test__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* 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.
*/

import { describe, expect, it } from 'vitest';

import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { ErrorValueObject } from '../../../../engine/value-object/base-value-object';
import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object';
import { getObjectValue } from '../../../__tests__/create-function-test-bed';
import { FUNCTION_NAMES_TEXT } from '../../function-names';
import { Find } from '../index';

describe('Test find function', () => {
const testFunction = new Find(FUNCTION_NAMES_TEXT.FIND);

describe('Find', () => {
it('Value is normal', () => {
const findText = StringValueObject.create('Univer');
const withinText = StringValueObject.create('Hello Univer');
const startNum = NumberValueObject.create(1);
const result = testFunction.calculate(findText, withinText, startNum);
expect(getObjectValue(result)).toStrictEqual(7);
});

it('FindText value test', () => {
const findText = NullValueObject.create();
const withinText = StringValueObject.create('Hello Univer');
const startNum = NumberValueObject.create(1);
const result = testFunction.calculate(findText, withinText, startNum);
expect(getObjectValue(result)).toStrictEqual(1);

const findText2 = BooleanValueObject.create(false);
const result2 = testFunction.calculate(findText2, withinText, startNum);
expect(getObjectValue(result2)).toStrictEqual(ErrorType.VALUE);

const findText3 = StringValueObject.create('test');
const result3 = testFunction.calculate(findText3, withinText, startNum);
expect(getObjectValue(result3)).toStrictEqual(ErrorType.VALUE);

const findText4 = ErrorValueObject.create(ErrorType.NAME);
const result4 = testFunction.calculate(findText4, withinText, startNum);
expect(getObjectValue(result4)).toStrictEqual(ErrorType.NAME);
});

it('WithinText value test', () => {
const findText = StringValueObject.create('Univer');
const withinText = NullValueObject.create();
const startNum = NumberValueObject.create(1);
const result = testFunction.calculate(findText, withinText, startNum);
expect(getObjectValue(result)).toStrictEqual(ErrorType.VALUE);

const withinText2 = BooleanValueObject.create(false);
const result2 = testFunction.calculate(findText, withinText2, startNum);
expect(getObjectValue(result2)).toStrictEqual(ErrorType.VALUE);

const withinText3 = StringValueObject.create('test');
const result3 = testFunction.calculate(findText, withinText3, startNum);
expect(getObjectValue(result3)).toStrictEqual(ErrorType.VALUE);

const withinText4 = ErrorValueObject.create(ErrorType.NAME);
const result4 = testFunction.calculate(findText, withinText4, startNum);
expect(getObjectValue(result4)).toStrictEqual(ErrorType.NAME);
});

it('StartNum value test', () => {
const findText = StringValueObject.create('Univer');
const withinText = StringValueObject.create('Hello Univer');
const startNum = NullValueObject.create();
const result = testFunction.calculate(findText, withinText, startNum);
expect(getObjectValue(result)).toStrictEqual(ErrorType.VALUE);

const startNum2 = BooleanValueObject.create(true);
const result2 = testFunction.calculate(findText, withinText, startNum2);
expect(getObjectValue(result2)).toStrictEqual(7);

const startNum3 = StringValueObject.create('test');
const result3 = testFunction.calculate(findText, withinText, startNum3);
expect(getObjectValue(result3)).toStrictEqual(ErrorType.VALUE);

const startNum4 = ErrorValueObject.create(ErrorType.NAME);
const result4 = testFunction.calculate(findText, withinText, startNum4);
expect(getObjectValue(result4)).toStrictEqual(ErrorType.NAME);

const startNum5 = NumberValueObject.create(13);
const result5 = testFunction.calculate(findText, withinText, startNum5);
expect(getObjectValue(result5)).toStrictEqual(ErrorType.VALUE);
});

it('Value is array', () => {
const findText = ArrayValueObject.create({
calculateValueList: transformToValueObject([
[1, ' ', '中文测试', true, false, null],
[0, 'm', '2.34', 'M', -3, ErrorType.NAME],
]),
rowCount: 2,
columnCount: 6,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
const withinText = StringValueObject.create('Miriam McGovern');
const startNum = NumberValueObject.create(2);
const result = testFunction.calculate(findText, withinText, startNum);
expect(getObjectValue(result)).toStrictEqual([
[ErrorType.VALUE, 7, ErrorType.VALUE, ErrorType.VALUE, ErrorType.VALUE, 2],
[ErrorType.VALUE, 6, ErrorType.VALUE, 8, ErrorType.VALUE, ErrorType.NAME],
]);
});

it('More test', () => {
const findText = StringValueObject.create('o');
const withinText = StringValueObject.create('Hello中文o😊Wo😊rld');
const startNum = ArrayValueObject.create('{1,6,9,13}');
const result = testFunction.calculate(findText, withinText, startNum);
expect(getObjectValue(result)).toStrictEqual([
[5, 8, 12, ErrorType.VALUE],
]);

const findText2 = StringValueObject.create('2');
const withinText2 = StringValueObject.create('2012-2-2');
const startNum2 = ArrayValueObject.create('{1,2,5,7}');
const result2 = testFunction.calculate(findText2, withinText2, startNum2);
expect(getObjectValue(result2)).toStrictEqual([
[1, 4, 6, 8],
]);
});
});
});
106 changes: 106 additions & 0 deletions packages/engine-formula/src/functions/text/find/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* 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.
*/

import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import { ErrorType } from '../../../basics/error-type';
import { getTextValueOfNumberFormat } from '../../../basics/format';
import { expandArrayValueObject } from '../../../engine/utils/array-object';
import { checkVariantsErrorIsStringToNumber } from '../../../engine/utils/check-variant-error';
import { type BaseValueObject, ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
import { BaseFunction } from '../../base-function';

export class Find extends BaseFunction {
override minParams = 2;

override maxParams = 3;

override calculate(findText: BaseValueObject, withinText: BaseValueObject, startNum?: BaseValueObject): BaseValueObject {
const _startNum = startNum ?? NumberValueObject.create(1);

const maxRowLength = Math.max(
findText.isArray() ? (findText as ArrayValueObject).getRowCount() : 1,
withinText.isArray() ? (withinText as ArrayValueObject).getRowCount() : 1,
_startNum.isArray() ? (_startNum as ArrayValueObject).getRowCount() : 1
);

const maxColumnLength = Math.max(
findText.isArray() ? (findText as ArrayValueObject).getColumnCount() : 1,
withinText.isArray() ? (withinText as ArrayValueObject).getColumnCount() : 1,
_startNum.isArray() ? (_startNum as ArrayValueObject).getColumnCount() : 1
);

const findTextArray = expandArrayValueObject(maxRowLength, maxColumnLength, findText, ErrorValueObject.create(ErrorType.NA));
const withinTextArray = expandArrayValueObject(maxRowLength, maxColumnLength, withinText, ErrorValueObject.create(ErrorType.NA));
const startNumArray = expandArrayValueObject(maxRowLength, maxColumnLength, _startNum, ErrorValueObject.create(ErrorType.NA));

const resultArray = findTextArray.mapValue((findTextObject, rowIndex, columnIndex) => {
const withinTextObject = withinTextArray.get(rowIndex, columnIndex) as BaseValueObject;
const startNumObject = startNumArray.get(rowIndex, columnIndex) as BaseValueObject;

if (findTextObject.isError()) {
return findTextObject;
}

if (withinTextObject.isError()) {
return withinTextObject;
}

if (startNumObject.isError()) {
return startNumObject;
}

return this._handleSingleObject(findTextObject, withinTextObject, startNumObject);
});

if (maxRowLength === 1 && maxColumnLength === 1) {
return (resultArray as ArrayValueObject).get(0, 0) as BaseValueObject;
}

return resultArray;
}

private _handleSingleObject(findText: BaseValueObject, withinText: BaseValueObject, startNum: BaseValueObject): BaseValueObject {
const findTextValue = getTextValueOfNumberFormat(findText);
const withinTextValue = getTextValueOfNumberFormat(withinText);

const { isError, errorObject, variants } = checkVariantsErrorIsStringToNumber(startNum);

if (isError) {
return errorObject as BaseValueObject;
}

const [startNumObject] = variants as BaseValueObject[];

const startNumValue = Math.floor(+startNumObject.getValue());

if (withinText.isNull() || startNumValue <= 0 || startNumValue > withinTextValue.length) {
return ErrorValueObject.create(ErrorType.VALUE);
}

if (findText.isNull() || findTextValue.length === 0) {
return NumberValueObject.create(startNumValue);
}

const result = withinTextValue.indexOf(findTextValue, startNumValue - 1);

if (result === -1) {
return ErrorValueObject.create(ErrorType.VALUE);
}

return NumberValueObject.create(result + 1);
}
}
Loading

0 comments on commit 114f6dc

Please sign in to comment.