Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(formula): add functions, fix function calculation error #1395

Merged
merged 14 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/shared/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @param val The number or string to be judged
* @returns Result
*/
export function isRealNum(val: string | number) {
export function isRealNum(val: string | number | boolean) {
if (val === null || val.toString().replace(/\s/g, '') === '') {
return false;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/engine-formula/src/basics/__tests__/date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { describe, expect, it } from 'vitest';
import { excelDateSerial, excelSerialToDate, formatDateDefault } from '../date';
import { excelDateSerial, excelSerialToDate, formatDateDefault, isValidDateStr } from '../date';

describe('Test date', () => {
it('Function excelDateSerial', () => {
Expand All @@ -32,4 +32,10 @@ describe('Test date', () => {
expect(formatDateDefault(excelSerialToDate(367))).toBe('1901/01/01');
expect(formatDateDefault(excelSerialToDate(45324))).toBe('2024/02/02');
});
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();
});
});
5 changes: 5 additions & 0 deletions packages/engine-formula/src/basics/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
import type {
BooleanNumber,
ICellData,
IColumnData,
IObjectArrayPrimitiveType,
IObjectMatrixPrimitiveType,
IRange,
IRowData,
IUnitRange,
Nullable,
ObjectMatrix,
Expand Down Expand Up @@ -46,6 +49,8 @@ export interface ISheetItem {
cellData: ObjectMatrix<ICellData>;
rowCount: number;
columnCount: number;
rowData: IObjectArrayPrimitiveType<Partial<IRowData>>;
columnData: IObjectArrayPrimitiveType<Partial<IColumnData>>;
}

export interface ISheetData {
Expand Down
35 changes: 35 additions & 0 deletions packages/engine-formula/src/basics/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,38 @@ export function formatDateDefault(date: Date): string {
// 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日"
* @param dateStr
* @returns
*/
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ export function createCommandTestBed(workbookConfig?: IWorkbookData, dependencie
cellData: new ObjectMatrix(sheetConfig.cellData),
rowCount: sheetConfig.rowCount,
columnCount: sheetConfig.columnCount,
rowData: sheetConfig.rowData,
columnData: sheetConfig.columnData,
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,14 @@ export class FunctionNode extends BaseAstNode {

for (let i = 0; i < childrenCount; i++) {
const object = children[i].getValue();

if (object == null) {
continue;
}
if (object.isReferenceObject()) {

// 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
variants.push((object as BaseReferenceObject).toArrayValueObject());
} else {
variants.push(object as BaseValueObject);
Expand Down Expand Up @@ -139,7 +143,7 @@ export class FunctionNode extends BaseAstNode {
const children = this.getChildren();
const childrenCount = children.length;

if (this._functionExecutor.name !== 'LOOKUP' || childrenCount !== 3) {
if (!this._functionExecutor.needsExpandParams || childrenCount !== 3) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,14 @@ export class BaseReferenceObject extends ObjectClassType {
return this.getCurrentActiveSheetData().columnCount;
}

getRowData() {
return this.getCurrentActiveSheetData().rowData;
}

getColumnData() {
return this.getCurrentActiveSheetData().columnData;
}

isCell() {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* 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 { truncateNumber } from '../math-kit';

describe('Test math kit', () => {
it('Function truncateNumber', () => {
expect(truncateNumber('1234567890123456')).toBe(1234567890123450);
expect(truncateNumber('123.4567890123456789')).toBe(123.456789012345);
expect(truncateNumber('0.1234567890123456789')).toBe(0.123456789012345);
expect(truncateNumber('1.234567890123456e+20')).toBe(123456789012345000000);
expect(truncateNumber('123456789012345')).toBe(123456789012345);
expect(truncateNumber('0.000000000000123456')).toBe(0.000000000000123456);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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 { convertTonNumber } from '../object-covert';
import { BooleanValueObject } from '../../value-object/primitive-object';

describe('Test object cover', () => {
it('Function convertTonNumber', () => {
expect(convertTonNumber(new BooleanValueObject(true)).getValue()).toBe(1);
expect(convertTonNumber(new BooleanValueObject(false)).getValue()).toBe(0);
});
});
44 changes: 44 additions & 0 deletions packages/engine-formula/src/engine/utils/math-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,50 @@ export function ceil(base: number, precision: number): number {
return Math.ceil(multiply(base, factor)) / factor;
}

export function mod(base: number, divisor: number): number {
const bigNumber = new Big(base);
const bigDivisor = new Big(divisor);

const quotient = Math.floor(base / divisor);

const result = bigNumber.minus(bigDivisor.times(quotient));

return result.toNumber();
}

export function pow(base: number, exponent: number): number {
return base ** exponent;
}

/**
* Excel can display numbers with up to about 15 digits of precision. This includes the sum of the integer part and the decimal part
* @param input
* @returns
*/
export function truncateNumber(input: number | string): number {
const num = new Big(input);
const numStr = num.toFixed(); // Convert to fixed-point notation

const parts = numStr.split('.');
let integerPart = parts[0];
let decimalPart = parts.length > 1 ? parts[1] : '';

// Handle integer part greater than 15 digits
if (integerPart.length > 15) {
integerPart = integerPart.slice(0, 15) + '0'.repeat(integerPart.length - 15);
}

// Handle decimal part for numbers with an integer part of '0' and leading zeros
if (integerPart === '0') {
const nonZeroIndex = decimalPart.search(/[1-9]/); // Find the first non-zero digit
if (nonZeroIndex !== -1 && nonZeroIndex + 15 < decimalPart.length) {
decimalPart = decimalPart.slice(0, nonZeroIndex + 15);
}
} else if (integerPart.length + decimalPart.length > 15) {
// Adjust decimal part if total length exceeds 15
decimalPart = decimalPart.slice(0, 15 - integerPart.length);
}

// Convert back to number, may cause precision loss for very large or small numbers
return Number.parseFloat(integerPart + (decimalPart ? `.${decimalPart}` : ''));
}
27 changes: 27 additions & 0 deletions packages/engine-formula/src/engine/utils/object-covert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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 { BaseValueObject } from '../value-object/base-value-object';
import { NumberValueObject } from '../value-object/primitive-object';

export function convertTonNumber(valueObject: BaseValueObject) {
const currentValue = valueObject.getValue();
let result = 0;
if (currentValue) {
result = 1;
}
return new NumberValueObject(result, true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('arrayValueObject test', () => {
});
expect(originValueObject.count()?.getValue()).toBe(6);
});
it('CountA', () => {
it('Counta', () => {
const originValueObject = new ArrayValueObject({
calculateValueList: transformToValueObject([
[1, ' ', 1.23, true, false],
Expand Down
Loading
Loading