Skip to content

Commit

Permalink
feat(formula): add MINIFS,AVERAGEIF,AVERAGEIFS functions (#2561)
Browse files Browse the repository at this point in the history
* feat(formula): minifs

* fix(formula): common util for xxxIFS function

* feat(formula): add averageif function

* feat(formula): add averageifs

* feat(formula): add countif

* feat(formula): add countifs

* feat(formula): add countifs

* feat(formula): add countblank function

* feat(formula): add mina function

* feat(formula): add maxa function

* feat(formula): add avedev function

* fix(formula): fix acot negative number calculation

* fix(formula): sumif reports ref error

* fix(formula): minifs compare string

* fix(formula): averageif supports reference object

* fix(formula): averageif referance

* fix(formula): countif test

* fix(formula): avedev, countif calculation error
  • Loading branch information
Dushusir authored Jul 1, 2024
1 parent fbf4548 commit 909f2ee
Show file tree
Hide file tree
Showing 43 changed files with 3,458 additions and 311 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ describe('Test compare', () => {

it('Function compareWithWildcard', () => {
expect(compareWithWildcard('test12', 'test*', compareToken.EQUALS)).toBe(true);
expect(compareWithWildcard('hello', 'test*', compareToken.NOT_EQUAL)).toBe(true);
expect(compareWithWildcard('test12', 'test*', compareToken.GREATER_THAN)).toBe(true);
expect(compareWithWildcard('test12', 'test*', compareToken.GREATER_THAN_OR_EQUAL)).toBe(true);
expect(compareWithWildcard('hello', 'test*', compareToken.LESS_THAN)).toBe(true);
expect(compareWithWildcard('hello', 'test*', compareToken.LESS_THAN_OR_EQUAL)).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

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

import { ArrayValueObject, transformToValue } from '../../value-object/array-value-object';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../value-object/array-value-object';
import { BooleanValueObject, NumberValueObject, StringValueObject } from '../../value-object/primitive-object';
import { valueObjectCompare } from '../object-compare';
import { compareToken } from '../../../basics/token';
import { ErrorType } from '../../../basics/error-type';
import { getObjectValue } from '../../../functions/__tests__/create-function-test-bed';

const range = ArrayValueObject.create(/*ts*/ `{
Ada;
Expand Down Expand Up @@ -208,5 +210,23 @@ describe('Test object compare', () => {
expect(value.getValue()).toStrictEqual(result[i]);
});
});
it('Array contains multi types cell, and compare string', () => {
const array = ArrayValueObject.create({
calculateValueList: transformToValueObject([
[1, ' ', 1.23, true, false, null],
[0, '100', '2.34', 'test', -3, ErrorType.NAME],
]),
rowCount: 2,
columnCount: 6,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
const str = StringValueObject.create('> ');

const value = valueObjectCompare(array, str);
expect(getObjectValue(value)).toStrictEqual([[false, false, false, true, true, false], [false, false, false, true, false, ErrorType.NAME]]);
});
});
});
4 changes: 3 additions & 1 deletion packages/engine-formula/src/engine/utils/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export function compareWithWildcard(currentValue: string, value: string, operato
case compareToken.EQUALS:
result = isMatchWildcard(currentValue, value);
break;

case compareToken.NOT_EQUAL:
result = !isMatchWildcard(currentValue, value);
break;
case compareToken.GREATER_THAN:
case compareToken.GREATER_THAN_OR_EQUAL:
result = isMatchWildcard(currentValue, value) || currentValue > replaceWildcard(value);
Expand Down
143 changes: 141 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, findCompareToken, valueObjectCompare } from './object-compare';

export function convertTonNumber(valueObject: BaseValueObject) {
const currentValue = valueObject.getValue();
Expand Down Expand Up @@ -129,3 +133,138 @@ 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);

const [, criteriaStringObject] = findCompareToken(`${criteriaValueObject.getValue()}`);

// When comparing non-numbers and numbers, countifs does not take the result
if (isNumberSensitive) {
resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject);
}

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;
}

/**
* Two valueObjects of the same type can be compared
* @param array
* @param range
* @param criteria
* @returns
*/
export function filterSameValueObjectResult(array: ArrayValueObject, range: ArrayValueObject, criteria: BaseValueObject) {
return array.mapValue((valueObject, r, c) => {
const rangeValueObject = range.get(r, c);
if (rangeValueObject && isSameValueObjectType(rangeValueObject, criteria)) {
return valueObject;
} else if (rangeValueObject?.isError() && criteria.isError() && rangeValueObject.getValue() === criteria.getValue()) {
return BooleanValueObject.create(true);
} else {
return BooleanValueObject.create(false);
}
});
}

/**
* Check if the two valueObjects are of the same type
* @param left
* @param right
* @returns
*/
export function isSameValueObjectType(left: BaseValueObject, right: BaseValueObject) {
if (left.isNumber() && right.isNumber()) {
return true;
}

if (left.isBoolean() && right.isBoolean()) {
return true;
}

// blank string is same as a blank cell
const isLeftBlank = left.isString() && left.getValue() === '';
const isRightBlank = right.isString() && right.getValue() === '';

if ((isLeftBlank || left.isNull()) && (isRightBlank || right.isNull())) {
return true;
}

if (left.isString() && !isLeftBlank && right.isString() && !isRightBlank) {
return true;
}

return 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 Expand Up @@ -1431,6 +1429,7 @@ export class ArrayValueObject extends BaseValueObject {
return newArray;
}

// eslint-disable-next-line max-lines-per-function, complexity
private _batchOperatorValue(
valueObject: BaseValueObject,
column: number,
Expand Down Expand Up @@ -1619,7 +1618,13 @@ export class ArrayValueObject extends BaseValueObject {
r + startRow
);
} else if (currentValue.isNull()) {
CELL_INVERTED_INDEX_CACHE.set(unitId, sheetId, column + startColumn, null, r + startRow);
// In comparison operations, these two situations are equivalent

// ">"&A1 (A1 is an empty cell)
// ">"

// So the empty cell is also cached as an empty string so that it can be retrieved next time
CELL_INVERTED_INDEX_CACHE.set(unitId, sheetId, column + startColumn, '', r + startRow);
} else {
CELL_INVERTED_INDEX_CACHE.set(
unitId,
Expand Down
Loading

0 comments on commit 909f2ee

Please sign in to comment.