Skip to content

Commit

Permalink
feat(formula): add functions, fix function calculation error (#1395)
Browse files Browse the repository at this point in the history
* feat(formula): add row function

* feat(formula): add column function

* feat(formula): add rows, columns function

* feat(formula): add index function

* test(formula): index function handles row number,column number

* feat(formula): add iserror, iserr functions

* feat(formula): add iserr, islogical, isna, isnontext, isnumber, isref, istext, power functions

* feat(formula): add mod function

* feat(formula): add subtotal function, supports sum

* test(formula): add test for SUM, AVERAGE, COUNT,COUNTA, MAX,MIN

* feat(formula): add var,varp,var.s,var.p,vara,varpa functions

* feat(formula): add std functions

* feat(formula): subtotal product

* fix(formula): edit formula description
  • Loading branch information
Dushusir authored Mar 4, 2024
1 parent 1d6e257 commit 885ba4b
Show file tree
Hide file tree
Showing 133 changed files with 6,936 additions and 676 deletions.
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
8 changes: 6 additions & 2 deletions packages/engine-formula/src/engine/ast-node/function-node.ts
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

0 comments on commit 885ba4b

Please sign in to comment.