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 MINIFS,AVERAGEIF,AVERAGEIFS functions #2561

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { describe, expect, it } from 'vitest';

import { ArrayValueObject, transformToValue } from '../../value-object/array-value-object';
import { BooleanValueObject, NumberValueObject, StringValueObject } from '../../value-object/primitive-object';
import { valueObjectCompare } from '../object-compare';
import { isNumericComparison, valueObjectCompare } from '../object-compare';
import { compareToken } from '../../../basics/token';

const range = ArrayValueObject.create(/*ts*/ `{
Expand Down Expand Up @@ -208,5 +208,16 @@ describe('Test object compare', () => {
expect(value.getValue()).toStrictEqual(result[i]);
});
});
it('Function isNumericComparison', () => {
expect(isNumericComparison('>40')).toBe(true);
expect(isNumericComparison('<=100')).toBe(true);
expect(isNumericComparison('=5')).toBe(true);
expect(isNumericComparison('test*')).toBe(false);
expect(isNumericComparison('=test')).toBe(false);
expect(isNumericComparison('> 40')).toBe(true);
expect(isNumericComparison('>=3.14')).toBe(true);
expect(isNumericComparison(5)).toBe(true);
expect(isNumericComparison(true)).toBe(false);
});
});
});
14 changes: 14 additions & 0 deletions packages/engine-formula/src/engine/utils/object-compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,17 @@ export function booleanObjectIntersection(valueObject1: BaseValueObject, valueOb
return BooleanValueObject.create(false);
});
}

export function isNumericComparison(condition: string | number | boolean): boolean {
if (typeof condition === 'number') {
return true;
} else if (typeof condition === 'boolean') {
return false;
}

// Combined regular expression for numeric comparisons
const numericComparisonPattern = /^[<>]?=?\s*\d+(\.\d+)?$/;

// Test the condition against the pattern
return numericComparisonPattern.test(condition.trim());
}
102 changes: 100 additions & 2 deletions packages/engine-formula/src/engine/utils/value-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import type { ICellData, Nullable } from '@univerjs/core';
import { CellValueType } from '@univerjs/core';
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
import type { ArrayValueObject } from '../value-object/array-value-object';
import type { BaseValueObject, ErrorValueObject } from '../value-object/base-value-object';
import { NumberValueObject } from '../value-object/primitive-object';
import type { BaseValueObject } from '../value-object/base-value-object';
import { ErrorValueObject } from '../value-object/base-value-object';
import { BooleanValueObject, NumberValueObject } from '../value-object/primitive-object';
import { ErrorType } from '../../basics/error-type';
import { expandArrayValueObject } from './array-object';
import { booleanObjectIntersection, isNumericComparison, valueObjectCompare } from './object-compare';

export function convertTonNumber(valueObject: BaseValueObject) {
const currentValue = valueObject.getValue();
Expand Down Expand Up @@ -129,3 +133,97 @@ export function objectValueToCellValue(objectValue: Nullable<BaseValueObject>):
};
}
}

/**
* The size of the extended range is determined by the maximum width and height of the criteria range.
* @param variants
* @returns
*/
export function calculateMaxDimensions(variants: BaseValueObject[]) {
let maxRowLength = 0;
let maxColumnLength = 0;

variants.forEach((variant, i) => {
if (i % 2 === 1) {
if (variant.isArray()) {
const arrayValue = variant as ArrayValueObject;
maxRowLength = Math.max(maxRowLength, arrayValue.getRowCount());
maxColumnLength = Math.max(maxColumnLength, arrayValue.getColumnCount());
} else {
maxRowLength = Math.max(maxRowLength, 1);
maxColumnLength = Math.max(maxColumnLength, 1);
}
}
});

return { maxRowLength, maxColumnLength };
}

export function getErrorArray(variants: BaseValueObject[], sumRange: BaseValueObject, maxRowLength: number, maxColumnLength: number) {
const sumRowLength = (sumRange as ArrayValueObject).getRowCount();
const sumColumnLength = (sumRange as ArrayValueObject).getColumnCount();

for (let i = 0; i < variants.length; i++) {
if (i % 2 === 1) continue;

const range = variants[i];

const rangeRowLength = (range as ArrayValueObject).getRowCount();
const rangeColumnLength = (range as ArrayValueObject).getColumnCount();
if (rangeRowLength !== sumRowLength || rangeColumnLength !== sumColumnLength) {
return expandArrayValueObject(maxRowLength, maxColumnLength, ErrorValueObject.create(ErrorType.VALUE));
}
}

return null;
}

export function getBooleanResults(variants: BaseValueObject[], maxRowLength: number, maxColumnLength: number, isNumberSensitive: boolean = false) {
const booleanResults: BaseValueObject[][] = [];

for (let i = 0; i < variants.length; i++) {
if (i % 2 === 1) continue;

const range = variants[i];
const criteria = variants[i + 1];
const criteriaArray = expandArrayValueObject(maxRowLength, maxColumnLength, criteria, ErrorValueObject.create(ErrorType.NA));

criteriaArray.iterator((criteriaValueObject, rowIndex, columnIndex) => {
if (!criteriaValueObject) {
return;
}

// range must be an ArrayValueObject, criteria must be a BaseValueObject
let resultArrayObject = valueObjectCompare(range, criteriaValueObject);

// When comparing non-numbers and numbers, countifs does not take the result
const isNumeric = isNumericComparison(criteriaValueObject.getValue());
if (isNumberSensitive && isNumeric) {
resultArrayObject = removeNonNumberValueObject(resultArrayObject as ArrayValueObject, range as ArrayValueObject);
}

if (booleanResults[rowIndex] === undefined) {
booleanResults[rowIndex] = [];
}

if (booleanResults[rowIndex][columnIndex] === undefined) {
booleanResults[rowIndex][columnIndex] = resultArrayObject;
return;
}

booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject);
});
}

return booleanResults;
}

function removeNonNumberValueObject(array: ArrayValueObject, range: ArrayValueObject) {
return array.mapValue((valueObject, r, c) => {
if (range.get(r, c)?.isNumber()) {
return valueObject;
} else {
return BooleanValueObject.create(false);
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,17 @@ describe('arrayValueObject test', () => {
it('CountBlank', () => {
const originValueObject = ArrayValueObject.create({
calculateValueList: transformToValueObject([
[1, ' ', 1.23, true, false],
[0, '100', '2.34', 'test', -3],
[1, ' ', 1.23, true, false, '', null],
[0, '100', '2.34', 'test', -3, ErrorType.VALUE, null],
]),
rowCount: 2,
columnCount: 5,
columnCount: 7,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
expect(originValueObject.countBlank()?.getValue()).toBe(0);
expect(originValueObject.countBlank()?.getValue()).toBe(3);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -892,11 +892,9 @@ export class ArrayValueObject extends BaseValueObject {
override countBlank() {
let accumulatorAll: BaseValueObject = NumberValueObject.create(0);
this.iterator((valueObject) => {
if (valueObject != null && !valueObject.isNull()) {
return true; // continue
if (valueObject == null || valueObject.isNull() || (valueObject.getValue() === '')) {
accumulatorAll = accumulatorAll.plusBy(1) as BaseValueObject;
}

accumulatorAll = accumulatorAll.plusBy(1) as BaseValueObject;
});

return accumulatorAll;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,37 @@ export class BooleanValueObject extends BaseValueObject {
}

override compare(valueObject: BaseValueObject, operator: compareToken): BaseValueObject {
return this._convertTonNumber().compare(valueObject, operator);
if (valueObject.isArray()) {
return valueObject.compare(this, reverseCompareOperator(operator));
}

return this.compareBy(valueObject.getValue(), operator);
}

override compareBy(value: string | number | boolean, operator: compareToken): BaseValueObject {
let result = false;
// FALSE > 0 and FALSE > "Univer" get TRUE
if (typeof value === 'string' || typeof value === 'number') {
result = this._compareString(operator);
} else if (typeof value === 'boolean') {
const booleanNumber = NumberValueObject.create(value ? 1 : 0);
return this._convertTonNumber().compare(booleanNumber, operator);
}

return BooleanValueObject.create(result);
}

private _compareString(operator: compareToken): boolean {
switch (operator) {
case compareToken.EQUALS:
case compareToken.GREATER_THAN:
case compareToken.GREATER_THAN_OR_EQUAL:
return true;
case compareToken.LESS_THAN:
case compareToken.LESS_THAN_OR_EQUAL:
case compareToken.NOT_EQUAL:
return false;
}
}

override concatenateFront(valueObject: BaseValueObject): BaseValueObject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Test acot function', () => {
});
const result = testFunction.calculate(valueArray);
expect(transformToValue(result.getArrayValue())).toStrictEqual([[0.7853981633974483, '#VALUE!', 0.682622552417217, 0.7853981633974483, 1.5707963267948966],
[1.5707963267948966, 0.009999666686665238, 0.40385979490737667, '#VALUE!', -0.3217505543966422]]);
[1.5707963267948966, 0.009999666686665238, 0.40385979490737667, '#VALUE!', 2.819842099193151]]);
});
});
});
9 changes: 8 additions & 1 deletion packages/engine-formula/src/functions/math/acot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ function acot(num: BaseValueObject) {
return ErrorValueObject.create(ErrorType.VALUE);
}

const result = Math.atan(1 / Number(currentValue));
currentValue = Number(currentValue);

let result = Math.atan(1 / currentValue);

// When the input value is negative, adjust the result to [0, π]
if (currentValue < 0) {
result += Math.PI;
}

if (Number.isNaN(result)) {
return ErrorValueObject.create(ErrorType.VALUE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { ArrayValueObject, transformToValue } from '../../../../engine/value-obj
import { FUNCTION_NAMES_MATH } from '../../function-names';
import { Sumif } from '../index';
import { StringValueObject } from '../../../../engine/value-object/primitive-object';
import { ErrorValueObject } from '../../../../engine/value-object/base-value-object';
import { ErrorType } from '../../../../basics/error-type';

describe('Test sumif function', () => {
const testFunction = new Sumif(FUNCTION_NAMES_MATH.SUMIF);
Expand Down Expand Up @@ -78,5 +80,18 @@ describe('Test sumif function', () => {
const resultObject = testFunction.calculate(range, criteria);
expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[4], [4], [44], [444]]);
});
it('Includes REF error', async () => {
const range = ErrorValueObject.create(ErrorType.REF);

const criteria = ArrayValueObject.create(/*ts*/ `{
4;
4;
44;
444
}`);

const resultObject = testFunction.calculate(range, criteria);
expect(resultObject.getValue()).toStrictEqual(ErrorType.REF);
});
});
});
12 changes: 10 additions & 2 deletions packages/engine-formula/src/functions/math/sumif/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,16 @@ export class Sumif extends BaseFunction {
override maxParams = 3;

override calculate(range: BaseValueObject, criteria: BaseValueObject, sumRange?: BaseValueObject) {
if (range.isError() || criteria.isError() || sumRange?.isError()) {
return ErrorValueObject.create(ErrorType.NA);
if (range.isError()) {
return range;
}

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

if (sumRange?.isError()) {
return sumRange;
}

if (!range.isArray() || (sumRange && !sumRange.isArray())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { ArrayValueObject, transformToValue } from '../../../../engine/value-obj
import { FUNCTION_NAMES_MATH } from '../../function-names';
import { Sumifs } from '../index';
import { NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object';
import { ErrorValueObject } from '../../../../engine/value-object/base-value-object';
import { ErrorType } from '../../../../basics/error-type';

describe('Test sumifs function', () => {
const testFunction = new Sumifs(FUNCTION_NAMES_MATH.SUMIF);
Expand Down Expand Up @@ -158,5 +160,19 @@ describe('Test sumifs function', () => {
const resultObject = testFunction.calculate(sumRange, range1, criteria1, range2, criteria2);
expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1], [0], [0], [0]]);
});

it('Includes REF error', async () => {
const range = ErrorValueObject.create(ErrorType.REF);

const criteria = ArrayValueObject.create(/*ts*/ `{
4;
4;
44;
444
}`);

const resultObject = testFunction.calculate(range, criteria);
expect(resultObject.getValue()).toStrictEqual(ErrorType.REF);
});
});
});