diff --git a/packages/engine-formula/src/engine/utils/__tests__/compare.spec.ts b/packages/engine-formula/src/engine/utils/__tests__/compare.spec.ts index f4062adb11c..9c623f147d2 100644 --- a/packages/engine-formula/src/engine/utils/__tests__/compare.spec.ts +++ b/packages/engine-formula/src/engine/utils/__tests__/compare.spec.ts @@ -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); }); }); diff --git a/packages/engine-formula/src/engine/utils/__tests__/object-compare.spec.ts b/packages/engine-formula/src/engine/utils/__tests__/object-compare.spec.ts index 00cba77ea98..ad34bc5d23c 100644 --- a/packages/engine-formula/src/engine/utils/__tests__/object-compare.spec.ts +++ b/packages/engine-formula/src/engine/utils/__tests__/object-compare.spec.ts @@ -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; @@ -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]]); + }); }); }); diff --git a/packages/engine-formula/src/engine/utils/compare.ts b/packages/engine-formula/src/engine/utils/compare.ts index 98ce7eaf427..942d7df66d1 100644 --- a/packages/engine-formula/src/engine/utils/compare.ts +++ b/packages/engine-formula/src/engine/utils/compare.ts @@ -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); diff --git a/packages/engine-formula/src/engine/utils/value-object.ts b/packages/engine-formula/src/engine/utils/value-object.ts index 5d9b018320a..230c1812516 100644 --- a/packages/engine-formula/src/engine/utils/value-object.ts +++ b/packages/engine-formula/src/engine/utils/value-object.ts @@ -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(); @@ -129,3 +133,138 @@ export function objectValueToCellValue(objectValue: Nullable): }; } } + +/** + * 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; +} diff --git a/packages/engine-formula/src/engine/value-object/__tests__/array-value-object.spec.ts b/packages/engine-formula/src/engine/value-object/__tests__/array-value-object.spec.ts index d5230ba0acb..c4158c55151 100644 --- a/packages/engine-formula/src/engine/value-object/__tests__/array-value-object.spec.ts +++ b/packages/engine-formula/src/engine/value-object/__tests__/array-value-object.spec.ts @@ -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); }); }); diff --git a/packages/engine-formula/src/engine/value-object/array-value-object.ts b/packages/engine-formula/src/engine/value-object/array-value-object.ts index 3251ccc0901..893f5b0c0b8 100644 --- a/packages/engine-formula/src/engine/value-object/array-value-object.ts +++ b/packages/engine-formula/src/engine/value-object/array-value-object.ts @@ -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; @@ -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, @@ -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, diff --git a/packages/engine-formula/src/engine/value-object/primitive-object.ts b/packages/engine-formula/src/engine/value-object/primitive-object.ts index 49c53b3e489..5251e659b0d 100644 --- a/packages/engine-formula/src/engine/value-object/primitive-object.ts +++ b/packages/engine-formula/src/engine/value-object/primitive-object.ts @@ -250,38 +250,71 @@ export class BooleanValueObject extends BaseValueObject { } override plus(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().plus(valueObject); + return this._convertToNumber().plus(valueObject); } override minus(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().minus(valueObject); + return this._convertToNumber().minus(valueObject); } override multiply(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().multiply(valueObject); + return this._convertToNumber().multiply(valueObject); } override divided(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().divided(valueObject); + return this._convertToNumber().divided(valueObject); } override mod(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().mod(valueObject); + return this._convertToNumber().mod(valueObject); } override compare(valueObject: BaseValueObject, operator: compareToken): BaseValueObject { - return this._convertTonNumber().compare(valueObject, operator); + if (valueObject.isArray()) { + return valueObject.compare(this, reverseCompareOperator(operator)); + } + + if (valueObject.isNull()) { + return this._convertToNumber().compare(valueObject, 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._convertToNumber().compare(booleanNumber, operator); + } + + return BooleanValueObject.create(result); + } + + private _compareString(operator: compareToken): boolean { + switch (operator) { + case compareToken.GREATER_THAN: + case compareToken.GREATER_THAN_OR_EQUAL: + return true; + case compareToken.EQUALS: + case compareToken.LESS_THAN: + case compareToken.LESS_THAN_OR_EQUAL: + case compareToken.NOT_EQUAL: + return false; + } } override concatenateFront(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().concatenateFront(valueObject); + return this._convertToNumber().concatenateFront(valueObject); } override concatenateBack(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().concatenateBack(valueObject); + return this._convertToNumber().concatenateBack(valueObject); } - private _convertTonNumber() { + private _convertToNumber() { const currentValue = this.getValue(); let result = 0; if (currentValue) { @@ -291,87 +324,87 @@ export class BooleanValueObject extends BaseValueObject { } override pow(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().pow(valueObject); + return this._convertToNumber().pow(valueObject); } override sqrt(): BaseValueObject { - return this._convertTonNumber().sqrt(); + return this._convertToNumber().sqrt(); } override cbrt(): BaseValueObject { - return this._convertTonNumber().cbrt(); + return this._convertToNumber().cbrt(); } override cos(): BaseValueObject { - return this._convertTonNumber().cos(); + return this._convertToNumber().cos(); } override acos(): BaseValueObject { - return this._convertTonNumber().acos(); + return this._convertToNumber().acos(); } override acosh(): BaseValueObject { - return this._convertTonNumber().acosh(); + return this._convertToNumber().acosh(); } override sin(): BaseValueObject { - return this._convertTonNumber().sin(); + return this._convertToNumber().sin(); } override asin(): BaseValueObject { - return this._convertTonNumber().asin(); + return this._convertToNumber().asin(); } override asinh(): BaseValueObject { - return this._convertTonNumber().asinh(); + return this._convertToNumber().asinh(); } override tan(): BaseValueObject { - return this._convertTonNumber().tan(); + return this._convertToNumber().tan(); } override tanh(): BaseValueObject { - return this._convertTonNumber().tanh(); + return this._convertToNumber().tanh(); } override atan(): BaseValueObject { - return this._convertTonNumber().atan(); + return this._convertToNumber().atan(); } override atan2(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().atan2(valueObject); + return this._convertToNumber().atan2(valueObject); } override atanh(): BaseValueObject { - return this._convertTonNumber().atanh(); + return this._convertToNumber().atanh(); } override log(): BaseValueObject { - return this._convertTonNumber().log(); + return this._convertToNumber().log(); } override log10(): BaseValueObject { - return this._convertTonNumber().log10(); + return this._convertToNumber().log10(); } override exp(): BaseValueObject { - return this._convertTonNumber().exp(); + return this._convertToNumber().exp(); } override abs(): BaseValueObject { - return this._convertTonNumber().abs(); + return this._convertToNumber().abs(); } override round(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().round(valueObject); + return this._convertToNumber().round(valueObject); } override floor(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().floor(valueObject); + return this._convertToNumber().floor(valueObject); } override ceil(valueObject: BaseValueObject): BaseValueObject { - return this._convertTonNumber().ceil(valueObject); + return this._convertToNumber().ceil(valueObject); } override convertToNumberObjectValue() { diff --git a/packages/engine-formula/src/functions/information/isref/index.ts b/packages/engine-formula/src/functions/information/isref/index.ts index fae4366d6a9..b5d137933c2 100644 --- a/packages/engine-formula/src/functions/information/isref/index.ts +++ b/packages/engine-formula/src/functions/information/isref/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { BaseValueObject } from '../../../engine/value-object/base-value-object'; +import type { FunctionVariantType } from '../../../engine/reference-object/base-reference-object'; import { BooleanValueObject } from '../../../engine/value-object/primitive-object'; import { BaseFunction } from '../../base-function'; @@ -25,7 +25,7 @@ export class Isref extends BaseFunction { override needsReferenceObject = true; - override calculate(value: BaseValueObject) { + override calculate(value: FunctionVariantType) { if (value.isReferenceObject()) { return BooleanValueObject.create(true); } diff --git a/packages/engine-formula/src/functions/math/acot/__tests__/index.spec.ts b/packages/engine-formula/src/functions/math/acot/__tests__/index.spec.ts index 64499d2595d..cbac2fdbc89 100644 --- a/packages/engine-formula/src/functions/math/acot/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/math/acot/__tests__/index.spec.ts @@ -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]]); }); }); }); diff --git a/packages/engine-formula/src/functions/math/acot/index.ts b/packages/engine-formula/src/functions/math/acot/index.ts index 206f7b2a05d..b8d2f855948 100644 --- a/packages/engine-formula/src/functions/math/acot/index.ts +++ b/packages/engine-formula/src/functions/math/acot/index.ts @@ -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); diff --git a/packages/engine-formula/src/functions/math/sumif/__tests__/index.spec.ts b/packages/engine-formula/src/functions/math/sumif/__tests__/index.spec.ts index 5e141eb316d..5dbcf666f50 100644 --- a/packages/engine-formula/src/functions/math/sumif/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/math/sumif/__tests__/index.spec.ts @@ -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); @@ -39,6 +41,21 @@ describe('Test sumif function', () => { expect(resultObject.getValue()).toBe(488); }); + it('Range and criteria, different type', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + true + }`); + + const criteria = StringValueObject.create('>'); + + const sumRange = ArrayValueObject.create(/*ts*/ `{ + 1 + }`); + + const resultObject = testFunction.calculate(range, criteria, sumRange); + expect(resultObject.getValue()).toBe(0); + }); + it('Sum range with wildcard asterisk', async () => { const range = ArrayValueObject.create(/*ts*/ `{ Ada; @@ -78,5 +95,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); + }); }); }); diff --git a/packages/engine-formula/src/functions/math/sumif/index.ts b/packages/engine-formula/src/functions/math/sumif/index.ts index b4e79fa7c60..5ef802103f9 100644 --- a/packages/engine-formula/src/functions/math/sumif/index.ts +++ b/packages/engine-formula/src/functions/math/sumif/index.ts @@ -15,7 +15,8 @@ */ import { ErrorType } from '../../../basics/error-type'; -import { valueObjectCompare } from '../../../engine/utils/object-compare'; +import { findCompareToken, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { filterSameValueObjectResult } from '../../../engine/utils/value-object'; import type { ArrayValueObject } from '../../../engine/value-object/array-value-object'; import { type BaseValueObject, ErrorValueObject } from '../../../engine/value-object/base-value-object'; import { BaseFunction } from '../../base-function'; @@ -26,8 +27,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())) { @@ -42,7 +51,11 @@ export class Sumif extends BaseFunction { } private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, sumRange?: BaseValueObject) { - const resultArrayObject = valueObjectCompare(range, criteria); + let resultArrayObject = valueObjectCompare(range, criteria); + + const [, criteriaStringObject] = findCompareToken(`${criteria.getValue()}`); + // When comparing non-numbers and numbers, it does not take the result + resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); // sumRange has the same dimensions as range const sumRangeArray = sumRange diff --git a/packages/engine-formula/src/functions/math/sumifs/__tests__/index.spec.ts b/packages/engine-formula/src/functions/math/sumifs/__tests__/index.spec.ts index 2f46df8fb57..e384885c870 100644 --- a/packages/engine-formula/src/functions/math/sumifs/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/math/sumifs/__tests__/index.spec.ts @@ -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); @@ -43,6 +45,23 @@ describe('Test sumifs function', () => { expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2]]); }); + it('Range and criteria, compare string', async () => { + const sumRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + const range = ArrayValueObject.create(`{ + a; + b; + c + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(sumRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[0]]); + }); + it('Range and array criteria', async () => { const sumRange = ArrayValueObject.create(/*ts*/ `{ 1; @@ -158,5 +177,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); + }); }); }); diff --git a/packages/engine-formula/src/functions/math/sumifs/index.ts b/packages/engine-formula/src/functions/math/sumifs/index.ts index d6e6a7f7a68..02801a9af3e 100644 --- a/packages/engine-formula/src/functions/math/sumifs/index.ts +++ b/packages/engine-formula/src/functions/math/sumifs/index.ts @@ -15,8 +15,7 @@ */ import { ErrorType } from '../../../basics/error-type'; -import { expandArrayValueObject } from '../../../engine/utils/array-object'; -import { booleanObjectIntersection, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { calculateMaxDimensions, getBooleanResults, getErrorArray } from '../../../engine/utils/value-object'; import { ArrayValueObject } from '../../../engine/value-object/array-value-object'; import type { BaseValueObject, IArrayValueObject } from '../../../engine/value-object/base-value-object'; import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; @@ -29,7 +28,7 @@ export class Sumifs extends BaseFunction { override calculate(sumRange: BaseValueObject, ...variants: BaseValueObject[]) { if (sumRange.isError()) { - return ErrorValueObject.create(ErrorType.NA); + return sumRange; } if (!sumRange.isArray()) { @@ -46,61 +45,20 @@ export class Sumifs extends BaseFunction { return ErrorValueObject.create(ErrorType.VALUE); } - const sumRowLength = (sumRange as ArrayValueObject).getRowCount(); - const sumColumnLength = (sumRange as ArrayValueObject).getColumnCount(); - // The size of the extended range is determined by the maximum width and height of the criteria range. - let maxRowLength = 0; - let maxColumnLength = 0; + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); - 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); - } - } - }); - - const booleanResults: BaseValueObject[][] = []; - - 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.NA)); - } - - const criteria = variants[i + 1]; - const criteriaArray = expandArrayValueObject(maxRowLength, maxColumnLength, criteria, ErrorValueObject.create(ErrorType.NA)); + const errorArray = getErrorArray(variants, sumRange, maxRowLength, maxColumnLength); - criteriaArray.iterator((criteriaValueObject, rowIndex, columnIndex) => { - if (!criteriaValueObject) { - return; - } - - const resultArrayObject = valueObjectCompare(range, criteriaValueObject); - - if (booleanResults[rowIndex] === undefined) { - booleanResults[rowIndex] = []; - } + if (errorArray) { + return errorArray; + } - if (booleanResults[rowIndex][columnIndex] === undefined) { - booleanResults[rowIndex][columnIndex] = resultArrayObject; - return; - } + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); - booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject); - }); - } + return this._aggregateResults(sumRange, booleanResults); + } + private _aggregateResults(sumRange: BaseValueObject, booleanResults: BaseValueObject[][]): ArrayValueObject { const sumResults = booleanResults.map((row) => { return row.map((booleanResult) => { return (sumRange as ArrayValueObject).pick(booleanResult as ArrayValueObject).sum(); diff --git a/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts b/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts new file mode 100644 index 00000000000..65861182e3b --- /dev/null +++ b/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts @@ -0,0 +1,349 @@ +/** + * 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 { beforeEach, describe, expect, it } from 'vitest'; + +import { FUNCTION_NAMES_META } from '../../function-names'; +import { Compare } from '../index'; +import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { compareToken } from '../../../../basics/token'; +import { ErrorType } from '../../../../basics/error-type'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { getObjectValue } from '../../../__tests__/create-function-test-bed'; +import { CELL_INVERTED_INDEX_CACHE } from '../../../../basics/inverted-index-cache'; + +describe('Test compare function', () => { + const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + + beforeEach(() => { + // Cache will affect the calculation results + CELL_INVERTED_INDEX_CACHE.clear(); + }); + + describe('Compare', () => { + it('Comparing Boolean and number', () => { + const value1 = BooleanValueObject.create(false); + const value2 = NumberValueObject.create(2); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(result.getValue()).toBe(true); + }); + it('Comparing Boolean and string', () => { + const value1 = BooleanValueObject.create(false); + const value2 = StringValueObject.create('Univer'); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(result.getValue()).toBe(true); + }); + + it('Comparing Boolean false and blank cell', () => { + const value1 = BooleanValueObject.create(false); + const value2 = NullValueObject.create(); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(result.getValue()).toBe(false); + }); + + it('Comparing Boolean true and blank cell', () => { + const value1 = BooleanValueObject.create(true); + const value2 = NullValueObject.create(); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(result.getValue()).toBe(true); + }); + + it('Array contains multi types cell, compare number', () => { + const value1 = 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 value2 = NumberValueObject.create(1); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(getObjectValue(result)).toStrictEqual([[false, true, true, true, true, false], [false, true, true, true, false, ErrorType.NAME]]); + }); + + it('Array contains multi types cell, compare string', () => { + const value1 = 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 value2 = StringValueObject.create('tes'); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(getObjectValue(result)).toStrictEqual([[false, false, false, true, true, false], [false, false, false, true, false, ErrorType.NAME]]); + }); + + it('Array contains multi types cell, compare boolean', () => { + const value1 = 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 value2 = BooleanValueObject.create(false); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(getObjectValue(result)).toStrictEqual([[false, false, false, true, false, false], [false, false, false, false, false, ErrorType.NAME]]); + }); + + it('Array contains multi types cell, compare blank cell', () => { + const value1 = 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 value2 = NullValueObject.create(); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(getObjectValue(result)).toStrictEqual([[true, true, true, true, false, false], [false, true, true, true, false, ErrorType.NAME]]); + }); + + it('Array contains multi types cell, compare blank string', () => { + const value1 = 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 value2 = StringValueObject.create(''); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(getObjectValue(result)).toStrictEqual([[false, true, false, true, true, false], [false, false, false, true, false, ErrorType.NAME]]); + }); + + it('Array contains multi types cell, compare error', () => { + const value1 = 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 value2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [ErrorType.REF], + ]), + rowCount: 1, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + + testFunction.setCompareType(compareToken.GREATER_THAN); + const result = testFunction.calculate(value1, value2); + expect(getObjectValue(result)).toStrictEqual([[ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF], [ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.NAME]]); + }); + }); +}); +// describe('Test compare function2', () => { +// const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + +// describe('Compare', () => { + +// it('Array contains multi types cell, compare string', () => { +// const value1 = 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 value2 = StringValueObject.create('tes'); + +// testFunction.setCompareType(compareToken.GREATER_THAN); +// const result = testFunction.calculate(value1, value2); +// expect(getObjectValue(result)).toStrictEqual([[false, false, false, true, true, false], [false, false, false, true, false, ErrorType.NAME]]); +// }); +// }); +// }); +// describe('Test compare function3', () => { +// const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + +// describe('Compare', () => { + +// it('Array contains multi types cell, compare boolean', () => { +// const value1 = 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 value2 = BooleanValueObject.create(false); + +// testFunction.setCompareType(compareToken.GREATER_THAN); +// const result = testFunction.calculate(value1, value2); +// expect(getObjectValue(result)).toStrictEqual([[false, false, false, true, false, false], [false, false, false, false, false, ErrorType.NAME]]); +// }); +// }); +// }); +// describe('Test compare function4', () => { +// const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + +// describe('Compare', () => { + +// it('Array contains multi types cell, compare blank cell', () => { +// const value1 = 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 value2 = NullValueObject.create(); + +// testFunction.setCompareType(compareToken.GREATER_THAN); +// const result = testFunction.calculate(value1, value2); +// expect(getObjectValue(result)).toStrictEqual([[true, true, true, true, false, false], [false, true, true, true, false, ErrorType.NAME]]); +// }); +// }); +// }); +// describe('Test compare function5', () => { +// const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + +// describe('Compare', () => { + +// it('Array contains multi types cell, compare blank string', () => { +// const value1 = 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 value2 = StringValueObject.create(''); + +// testFunction.setCompareType(compareToken.GREATER_THAN); +// const result = testFunction.calculate(value1, value2); +// expect(getObjectValue(result)).toStrictEqual([[false, true, false, true, true, false], [false, false, false, true, false, ErrorType.NAME]]); +// }); +// }); +// }); +// describe('Test compare function6', () => { +// const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + +// describe('Compare', () => { + +// it('Array contains multi types cell, compare error', () => { +// const value1 = 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 value2 = ArrayValueObject.create({ +// calculateValueList: transformToValueObject([ +// [ErrorType.REF], +// ]), +// rowCount: 1, +// columnCount: 1, +// unitId: '', +// sheetId: '', +// row: 0, +// column: 0, +// }); + +// testFunction.setCompareType(compareToken.GREATER_THAN); +// const result = testFunction.calculate(value1, value2); +// expect(getObjectValue(result)).toStrictEqual([[ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF], [ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.REF, ErrorType.NAME]]); +// }); +// }); +// }); diff --git a/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts new file mode 100644 index 00000000000..61fae799520 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts @@ -0,0 +1,160 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Avedev } from '../index'; +import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorType } from '../../../../basics/error-type'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; + +describe('Test avedev function', () => { + const testFunction = new Avedev(FUNCTION_NAMES_STATISTICAL.AVEDEV); + + describe('Avedev', () => { + it('Var1 is number, var2 is number', () => { + const var1 = NumberValueObject.create(1); + const var2 = NumberValueObject.create(2); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0.5); + }); + it('Var1 is number, var2 is string', () => { + const var1 = NumberValueObject.create(1); + const var2 = StringValueObject.create('test'); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.VALUE); + }); + it('Var1 is number, var2 is string number', () => { + const var1 = NumberValueObject.create(1); + const var2 = StringValueObject.create('2'); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0.5); + }); + it('Var1 is number, var2 is boolean', () => { + const var1 = NumberValueObject.create(2); + + let var2 = BooleanValueObject.create(true); + let result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0.5); + + var2 = BooleanValueObject.create(false); + result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + }); + it('Var1 is number, var2 is null', () => { + const var1 = NumberValueObject.create(1); + const var2 = NullValueObject.create(); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0); + }); + it('Var1 is number, var2 is error', () => { + const var1 = NumberValueObject.create(1); + const var2 = ErrorValueObject.create(ErrorType.NA); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.NA); + }); + + it('Var1 is number, var2 is array includes error', () => { + const var1 = NumberValueObject.create(1); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, null], + [0, ErrorType.VALUE], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.VALUE); + }); + + it('Var1 is array includes blank cells', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [null], + [null], + ]), + rowCount: 2, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(ErrorType.NUM); + }); + + it('Var1 is number, var2 is array not includes error', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, ' ', 1.23, true, false, null], + [0, '100', '2.34', 'test', -3, null], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(24.344081632653065); + }); + it('Var1 is number, var2 is array not includes error, includes 0', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, 0, 1.23, true, false, 0], + [0, '100', '2.34', 0, -3, 0], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(16.469917355371898); + }); + + it('Var1 is number, var2 is array not includes boolean, includes 0', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, 0, 1.23, 0, 0, 0], + [0, '100', '2.34', 0, -3, 0], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(14.158934911242605); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/avedev/index.ts b/packages/engine-formula/src/functions/statistical/avedev/index.ts new file mode 100644 index 00000000000..910b827b267 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/avedev/index.ts @@ -0,0 +1,137 @@ +/** + * 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 { Nullable } from '@univerjs/core'; +import type { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +import type { BaseValueObject } from '../../../engine/value-object/base-value-object'; +import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { NumberValueObject } from '../../../engine/value-object/primitive-object'; +import { BaseFunction } from '../../base-function'; +import { createNewArray } from '../../../engine/utils/array-object'; +import { ErrorType } from '../../../basics/error-type'; + +export class Avedev extends BaseFunction { + override minParams = 1; + + override maxParams = 255; + + override calculate(...variants: BaseValueObject[]) { + let accumulatorSum: BaseValueObject = NumberValueObject.create(0); + let accumulatorCount: BaseValueObject = NumberValueObject.create(0); + + // First, calculate the average + for (let i = 0; i < variants.length; i++) { + let variant = variants[i]; + + if (variant.isString()) { + variant = variant.convertToNumberObjectValue(); + } + + if (variant.isError()) { + return variant; + } + + if (variant.isArray()) { + variant = filterNumberValueObject(variant as ArrayValueObject); + + if (variant.isError()) { + return variant; + } + + variants[i] = variant; + accumulatorSum = accumulatorSum.plus(variant.sum()); + + if (accumulatorSum.isError()) { + return accumulatorSum; + } + + accumulatorCount = accumulatorCount.plus(variant.count()); + } else if (!variant.isNull()) { + accumulatorSum = accumulatorSum.plus(variant); + accumulatorCount = accumulatorCount.plus(NumberValueObject.create(1)); + } + } + + // If there is no data in the range, this calculation cannot be performed and a #NUM! error will be generated. + if (accumulatorCount.getValue() === 0) { + return ErrorValueObject.create(ErrorType.NUM); + } + + const average = accumulatorSum.divided(accumulatorCount); + if (average.isError()) { + return average; + } + + // Then, calculate the average of the absolute deviations from the average + let accumulatorAveDev: BaseValueObject = NumberValueObject.create(0); + for (let i = 0; i < variants.length; i++) { + let variant = variants[i]; + + if (variant.isString()) { + variant = variant.convertToNumberObjectValue(); + } + + if (variant.isError()) { + return variant; + } + + if (variant.isArray()) { + // ignore strings and booleans + // const { newArray, count } = ignoreStringAndBoolean(variant as ArrayValueObject); + // variant = newArray; + // accumulatorCount = accumulatorCount.minus(NumberValueObject.create(count)); + + accumulatorAveDev = accumulatorAveDev.plus(variant.minus(average).abs().sum()); + if (accumulatorAveDev.isError()) { + return accumulatorAveDev; + } + } else if (!variant.isNull()) { + accumulatorAveDev = accumulatorAveDev.plus(variant.minus(average).abs()); + } + } + + return accumulatorAveDev.divided(accumulatorCount); + } +} + +/** + * Filter the number value object from the array + * @param array + * @returns + */ +function filterNumberValueObject(array: ArrayValueObject) { + const newArray: BaseValueObject[][] = []; + newArray[0] = []; + + let isError: ErrorValueObject | null = null; + + array.iterator((valueObject: Nullable, _rowIndex: number, _columnIndex: number) => { + if (valueObject?.isError()) { + isError = valueObject as ErrorValueObject; + return false; + } + + if (valueObject?.isNumber()) { + newArray[0].push(valueObject); + } + }); + + if (isError) { + return isError; + } + + return createNewArray(newArray, 1, newArray[0].length); +} diff --git a/packages/engine-formula/src/functions/statistical/average/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/average/__tests__/index.spec.ts index 25bede936f7..54c520d1fe8 100644 --- a/packages/engine-formula/src/functions/statistical/average/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/average/__tests__/index.spec.ts @@ -156,5 +156,41 @@ describe('Test average function', () => { const result = testFunction.calculate(var1); expect(result.getValue()).toBe(ErrorType.DIV_BY_ZERO); }); + + it('Var1 is number, var2 is array not includes error, includes 0', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, 0, 1.23, true, false, 0], + [0, '100', '2.34', 0, -3, 0], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(9.415454545454546); + }); + + it('Var1 is number, var2 is array not includes boolean, includes 0', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, 0, 1.23, 0, 0, 0], + [0, '100', '2.34', 0, -3, 0], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(7.966923076923077); + }); }); }); diff --git a/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts new file mode 100644 index 00000000000..ffb57ea6c46 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts @@ -0,0 +1,277 @@ +/** + * 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 { Injector } from '@wendellhu/redi'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import type { IWorkbookData } from '@univerjs/core'; +import { CellValueType, LocaleType } from '@univerjs/core'; +import { Lexer } from '../../../../engine/analysis/lexer'; +import type { LexerNode } from '../../../../engine/analysis/lexer-node'; +import { AstTreeBuilder } from '../../../../engine/analysis/parser'; +import type { BaseAstNode } from '../../../../engine/ast-node/base-ast-node'; +import { Interpreter } from '../../../../engine/interpreter/interpreter'; +import { IFormulaCurrentConfigService } from '../../../../services/current-data.service'; +import { IFunctionService } from '../../../../services/function.service'; +import { IFormulaRuntimeService } from '../../../../services/runtime.service'; +import { createFunctionTestBed } from '../../../__tests__/create-function-test-bed'; +import { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import type { BaseValueObject, ErrorValueObject } from '../../../../engine/value-object/base-value-object'; +import type { ArrayValueObject } from '../../../../engine/value-object/array-value-object'; +import { Averageif } from '../index'; +import { ErrorType } from '../../../../basics/error-type'; + +const getTestWorkbookData = (): IWorkbookData => { + return { + id: 'test', + appVersion: '3.0.0-alpha', + sheets: { + sheet1: { + id: 'sheet1', + cellData: { + 0: { + 0: { + v: 1, + t: CellValueType.NUMBER, + }, + 1: { + v: 2, + t: CellValueType.NUMBER, + }, + }, + 1: { + 0: { + v: 3, + t: CellValueType.NUMBER, + }, + 1: { + v: 4, + t: CellValueType.NUMBER, + }, + 2: { + v: 'B2', + t: CellValueType.STRING, + }, + 3: { + v: 'R2C2', + t: CellValueType.STRING, + }, + }, + 2: { + 0: { + v: 1, + t: CellValueType.NUMBER, + }, + 1: { + v: ' ', + t: CellValueType.STRING, + }, + 2: { + v: 1.23, + t: CellValueType.NUMBER, + }, + 3: { + v: true, + t: CellValueType.BOOLEAN, + }, + 4: { + v: false, + t: CellValueType.BOOLEAN, + }, + }, + 3: { + 0: { + v: 0, + t: CellValueType.NUMBER, + }, + 1: { + v: '100', + }, + 2: { + v: '2.34', + }, + 3: { + v: 'test', + t: CellValueType.STRING, + }, + 4: { + v: -3, + t: CellValueType.NUMBER, + }, + }, + 4: { + 3: { + v: 'test1', + t: CellValueType.STRING, + }, + }, + 5: { + 0: { + v: 'Tom', + t: CellValueType.STRING, + }, + 1: { + v: 'Sarah', + t: CellValueType.STRING, + }, + 3: { + v: 'Univer', + t: CellValueType.STRING, + }, + }, + 6: { + 0: { + v: 'Alex', + t: CellValueType.STRING, + }, + 1: { + v: 'Mickey', + t: CellValueType.STRING, + }, + }, + }, + }, + }, + locale: LocaleType.ZH_CN, + name: '', + sheetOrder: [], + styles: {}, + }; +}; +describe('Test isref function', () => { + let get: Injector['get']; + let lexer: Lexer; + let astTreeBuilder: AstTreeBuilder; + let interpreter: Interpreter; + let calculate: (formula: string) => (string | number | boolean | null)[][] | string | number | boolean; + + beforeEach(() => { + const testBed = createFunctionTestBed(getTestWorkbookData()); + + get = testBed.get; + + lexer = get(Lexer); + astTreeBuilder = get(AstTreeBuilder); + interpreter = get(Interpreter); + + const functionService = get(IFunctionService); + + const formulaCurrentConfigService = get(IFormulaCurrentConfigService); + + const formulaRuntimeService = get(IFormulaRuntimeService); + + formulaCurrentConfigService.load({ + formulaData: {}, + arrayFormulaCellData: {}, + forceCalculate: false, + dirtyRanges: [], + dirtyNameMap: {}, + dirtyDefinedNameMap: {}, + dirtyUnitFeatureMap: {}, + excludedCell: {}, + allUnitData: { + [testBed.unitId]: testBed.sheetData, + }, + dirtyUnitOtherFormulaMap: {}, + }); + + const sheetItem = testBed.sheetData[testBed.sheetId]; + + formulaRuntimeService.setCurrent( + 0, + 0, + sheetItem.rowCount, + sheetItem.columnCount, + testBed.sheetId, + testBed.unitId + ); + + functionService.registerExecutors( + new Averageif(FUNCTION_NAMES_STATISTICAL.AVERAGEIF) + ); + + calculate = (formula: string) => { + const lexerNode = lexer.treeBuilder(formula); + + const astNode = astTreeBuilder.parse(lexerNode as LexerNode); + + const result = interpreter.execute(astNode as BaseAstNode); + + if ((result as ErrorValueObject).isError()) { + return (result as ErrorValueObject).getValue(); + } else if ((result as ArrayValueObject).isArray()) { + return (result as ArrayValueObject).toValue(); + } + return (result as BaseValueObject).getValue(); + }; + }); + + describe('Averageif', () => { + it('Range and criteria', async () => { + const result = await calculate('=AVERAGEIF(A1:A4,">0")'); + + expect(result).toBe(1.6666666666666667); + }); + it('Range and criteria, compare number', async () => { + const result = await calculate('=AVERAGEIF(A6:A7,">1")'); + + expect(result).toBe(ErrorType.DIV_BY_ZERO); + }); + + it('Range number', async () => { + const result = await calculate('=AVERAGEIF(1,1)'); + + expect(result).toBe(1); + }); + + it('Range string', async () => { + const result = await calculate('=AVERAGEIF("test",">1")'); + + expect(result).toBe(ErrorType.DIV_BY_ZERO); + }); + + it('Range string, average range number', async () => { + const result = await calculate('=AVERAGEIF("test",1,1)'); + + expect(result).toBe(ErrorType.NA); + }); + + it('Range blank cell', async () => { + const result = await calculate('=AVERAGEIF(A5,">1")'); + + expect(result).toBe(ErrorType.DIV_BY_ZERO); + }); + + it('Average range with wildcard asterisk', async () => { + const result = await calculate('=AVERAGEIF(D4:D6,"test*",A1)'); + + expect(result).toBe(2); + }); + + it('ArrayValueObject range and ArrayValueObject criteria', async () => { + const result = await calculate('=AVERAGEIF(A3:F4,A3:F4)'); + + // [0][1] ErrorType.DIV_BY_ZERO refer to Google Sheets + expect(result).toStrictEqual([[1, ErrorType.DIV_BY_ZERO, 1.23, ErrorType.DIV_BY_ZERO, ErrorType.DIV_BY_ZERO, 0], [0, 100, 2.34, ErrorType.DIV_BY_ZERO, -3, 0]]); + }); + + it('ArrayValueObject range and string criteria', async () => { + const result = await calculate('=AVERAGEIF(A3:F4," ")'); + + expect(result).toBe(ErrorType.DIV_BY_ZERO); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/averageif/index.ts b/packages/engine-formula/src/functions/statistical/averageif/index.ts new file mode 100644 index 00000000000..18ff69a0c69 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageif/index.ts @@ -0,0 +1,133 @@ +/** + * 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 { ErrorType } from '../../../basics/error-type'; +import type { BaseReferenceObject, FunctionVariantType } from '../../../engine/reference-object/base-reference-object'; +import { createNewArray } from '../../../engine/utils/array-object'; +import { findCompareToken, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { filterSameValueObjectResult } from '../../../engine/utils/value-object'; +import type { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +import { type BaseValueObject, ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { BaseFunction } from '../../base-function'; + +export class Averageif extends BaseFunction { + override minParams = 2; + + override maxParams = 3; + + override needsReferenceObject = true; + + override calculate(range: FunctionVariantType, criteria: FunctionVariantType, averageRange?: FunctionVariantType) { + if (range.isError()) { + return range; + } + + if (criteria.isError()) { + return criteria; + } + + if (averageRange?.isError()) { + return averageRange; + } + + if (range.isReferenceObject()) { + range = (range as BaseReferenceObject).toArrayValueObject(); + } + + if (!range.isArray()) { + range = createNewArray([[range as BaseValueObject]], 1, 1); + } + + if (criteria.isReferenceObject()) { + criteria = (criteria as BaseReferenceObject).toArrayValueObject(); + } + + if (averageRange && !averageRange?.isReferenceObject()) { + return ErrorValueObject.create(ErrorType.NA); + } + + range = range as BaseValueObject; + criteria = criteria as BaseValueObject; + averageRange = averageRange as BaseReferenceObject; + + if (criteria.isArray()) { + return (criteria as ArrayValueObject).map((criteriaItem) => this._handleSingleObject(range, criteriaItem, averageRange)); + } + + return this._handleSingleObject(range, criteria, averageRange); + } + + private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseReferenceObject) { + let resultArrayObject = valueObjectCompare(range, criteria); + + const [, criteriaStringObject] = findCompareToken(`${criteria.getValue()}`); + // When comparing non-numbers and numbers, it does not take the result + resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); + + // averageRange has the same dimensions as range + let averageRangeArray = averageRange + ? this._createRangeReferenceObject(averageRange, range) + : (range as ArrayValueObject); + + if (!averageRangeArray) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + if (averageRangeArray.isError()) { + return averageRangeArray as ErrorValueObject; + } + + if (averageRangeArray.isReferenceObject()) { + averageRangeArray = (averageRangeArray as BaseReferenceObject).toArrayValueObject(); + } + + averageRangeArray = averageRangeArray as ArrayValueObject; + + const picked = averageRangeArray.pick(resultArrayObject as ArrayValueObject); + const sum = picked.sum(); + const count = picked.count(); + return sum.divided(count); + } + + /** + * Create reference object, starting from the first cell in the upper left corner, the height is rowCount and the width is columnCount + * @param averageRange + * @param rowCount + * @param columnCount + * @returns + */ + private _createRangeReferenceObject(averageRange: BaseReferenceObject, range: BaseValueObject) { + const averageRangeRow = averageRange.getRowCount(); + const averageRangeColumn = averageRange.getColumnCount(); + + const rowCount = range.isArray() ? (range as ArrayValueObject).getRowCount() : 1; + const columnCount = range.isArray() ? (range as ArrayValueObject).getColumnCount() : 1; + + if (averageRangeRow === rowCount && averageRangeColumn === columnCount) { + return averageRange; + } + + const { startRow, startColumn } = averageRange.getRangeData(); + const rangeData = { + startRow, + startColumn, + endRow: startRow + rowCount - 1, + endColumn: startColumn + columnCount - 1, + }; + + return this.createReferenceObject(averageRange, rangeData); + } +} diff --git a/packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts new file mode 100644 index 00000000000..f571aaae13f --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts @@ -0,0 +1,179 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Averageifs } from '../index'; +import { ArrayValueObject, transformToValue } from '../../../../engine/value-object/array-value-object'; +import { NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ErrorType } from '../../../../basics/error-type'; + +describe('Test averageifs function', () => { + const testFunction = new Averageifs(FUNCTION_NAMES_STATISTICAL.AVERAGEIFS); + + describe('Averageifs', () => { + it('Range and criteria', async () => { + const averageRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + const range = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(averageRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2.5]]); + }); + + it('Range and criteria, compare string', async () => { + const averageRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + const range = ArrayValueObject.create(`{ + a; + b; + c + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(averageRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[ErrorType.DIV_BY_ZERO]]); + }); + + it('Range and array criteria', async () => { + const averageRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const resultObject = testFunction.calculate(averageRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2.5], [3], [ErrorType.DIV_BY_ZERO]]); + }); + + it('2 ranges and criteria', async () => { + const averageRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = StringValueObject.create('>2'); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = StringValueObject.create('<5'); + + const resultObject = testFunction.calculate(averageRange, range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2]]); + }); + + it('2 ranges and criteria, 1 array criteria', async () => { + const averageRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = NumberValueObject.create(5); + + const resultObject = testFunction.calculate(averageRange, range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[3], [3], [ErrorType.DIV_BY_ZERO]]); + }); + + it('2 ranges and criteria, 2 array criteria', async () => { + const averageRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = ArrayValueObject.create(`{ + 4; + 4; + 4; + 4 + }`); + + const resultObject = testFunction.calculate(averageRange, range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2], [ErrorType.DIV_BY_ZERO], [ErrorType.DIV_BY_ZERO], [ErrorType.DIV_BY_ZERO]]); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/averageifs/index.ts b/packages/engine-formula/src/functions/statistical/averageifs/index.ts new file mode 100644 index 00000000000..0b52f77a738 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageifs/index.ts @@ -0,0 +1,83 @@ +/** + * 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 { ErrorType } from '../../../basics/error-type'; +import { calculateMaxDimensions, getBooleanResults, getErrorArray } from '../../../engine/utils/value-object'; +import { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +import type { BaseValueObject, IArrayValueObject } from '../../../engine/value-object/base-value-object'; +import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { BaseFunction } from '../../base-function'; + +export class Averageifs extends BaseFunction { + override minParams = 3; + + override maxParams = 255; + + override calculate(averageRange: BaseValueObject, ...variants: BaseValueObject[]) { + if (averageRange.isError()) { + return ErrorValueObject.create(ErrorType.NA); + } + + if (!averageRange.isArray()) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + // Range and criteria must be paired + if (variants.length % 2 !== 0) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + // Every range must be array + if (variants.some((variant, i) => i % 2 === 0 && !variant.isArray())) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); + + const errorArray = getErrorArray(variants, averageRange, maxRowLength, maxColumnLength); + + if (errorArray) { + return errorArray; + } + + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); + + return this._aggregateResults(averageRange, booleanResults); + } + + private _aggregateResults(averageRange: BaseValueObject, booleanResults: BaseValueObject[][]): ArrayValueObject { + const maxResults = booleanResults.map((row) => { + return row.map((booleanResult) => { + const picked = (averageRange as ArrayValueObject).pick(booleanResult as ArrayValueObject); + const sum = picked.sum(); + const count = picked.count(); + return sum.divided(count); + }); + }); + + const arrayValueObjectData: IArrayValueObject = { + calculateValueList: maxResults, + rowCount: maxResults.length, + columnCount: maxResults[0].length, + unitId: this.unitId || '', + sheetId: this.subUnitId || '', + row: this.row, + column: this.column, + }; + + return ArrayValueObject.create(arrayValueObjectData); + } +} diff --git a/packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts new file mode 100644 index 00000000000..aacf5caf56d --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts @@ -0,0 +1,94 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Countblank } from '../index'; +import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorType } from '../../../../basics/error-type'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; + +describe('Test countblank function', () => { + const testFunction = new Countblank(FUNCTION_NAMES_STATISTICAL.COUNTBLANK); + + describe('Countblank', () => { + it('Range is error', () => { + const range = ErrorValueObject.create(ErrorType.REF); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(ErrorType.REF); + }); + it('Range is number', () => { + const range = NumberValueObject.create(1); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(0); + }); + it('Range is string', () => { + const range = StringValueObject.create('test'); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(0); + }); + it('Range is string, blank string', () => { + const range = StringValueObject.create(''); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(1); + }); + it('Range is boolean', () => { + const range = BooleanValueObject.create(true); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(0); + }); + it('Range is null', () => { + const range = NullValueObject.create(); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(1); + }); + + it('Range is array', () => { + const range = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, ' ', 1.23, true, false, '', null], + [0, '100', '2.34', 'test', -3, ErrorType.VALUE, null], + ]), + rowCount: 2, + columnCount: 7, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(3); + }); + + it('Range is array with ref', () => { + const range = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [ErrorType.REF], + ]), + rowCount: 1, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(0); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/countblank/index.ts b/packages/engine-formula/src/functions/statistical/countblank/index.ts new file mode 100644 index 00000000000..02ee7ea98d5 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countblank/index.ts @@ -0,0 +1,42 @@ +/** + * 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 '../../../engine/value-object/base-value-object'; +import { NumberValueObject } from '../../../engine/value-object/primitive-object'; +import { BaseFunction } from '../../base-function'; + +export class Countblank extends BaseFunction { + override minParams = 1; + + override maxParams = 1; + + override calculate(variant: BaseValueObject) { + if (variant.isError()) { + return variant; + } + + if (variant.getValue() === '' || variant.isNull()) { + return NumberValueObject.create(1); + } + + if (!variant.isArray()) { + return NumberValueObject.create(0); + } + + return variant.countBlank(); + } +} + diff --git a/packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts new file mode 100644 index 00000000000..ecbdfe5572a --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts @@ -0,0 +1,151 @@ +/** + * 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 { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Countif } from '../index'; +import { StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ErrorType } from '../../../../basics/error-type'; + +describe('Test countif function', () => { + const testFunction = new Countif(FUNCTION_NAMES_STATISTICAL.COUNTIF); + + describe('Countif', () => { + it('Range and criteria', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + 1; + 4; + 44; + 444; + Univer + }`); + + const criteria = StringValueObject.create('>40'); + + const resultObject = testFunction.calculate(range, criteria); + expect(resultObject.getValue()).toBe(2); + }); + + it('Average range with wildcard asterisk', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + Ada; + test1; + test12; + Univer + }`); + + const criteria = StringValueObject.create('test*'); + + const resultObject = testFunction.calculate(range, criteria); + expect(resultObject.getValue()).toBe(2); + }); + + it('Average range with boolean', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + TRUE; + FALSE + }`); + + const criteria = StringValueObject.create('>FALSE'); + + const resultObject = testFunction.calculate(range, criteria); + expect(resultObject.getValue()).toBe(1); + }); + + it('ArrayValueObject range and ArrayValueObject criteria', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + 1; + 4; + 44; + 444 + }`); + + const criteria = ArrayValueObject.create(/*ts*/ `{ + 4; + 4; + 44; + 444 + }`); + + const resultObject = testFunction.calculate(range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1], [1], [1], [1]]); + }); + + it('ArrayValueObject range and ArrayValueObject criteria multi type cell', async () => { + const range = 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 criteria = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + ['>1', '> ', '>1.23', '>true', '>false', '>'], + ['>0', '>100', '>2.34', '>test', '>-3', ErrorType.NAME], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + + const resultObject = testFunction.calculate(range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[3, 1, 2, 0, 1, 0], [4, 0, 1, 0, 5, 1]]); + }); + + it('ArrayValueObject range equals ArrayValueObject criteria multi types cell', async () => { + const range = 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 criteria = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + ['=1', '= ', '=1.23', '=true', '=false', '='], + ['=0', '=100', '=2.34', '=test', '=-3', ErrorType.NAME], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + + const resultObject = testFunction.calculate(range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/countif/index.ts b/packages/engine-formula/src/functions/statistical/countif/index.ts new file mode 100644 index 00000000000..638ea40e7c9 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countif/index.ts @@ -0,0 +1,69 @@ +/** + * 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 { ErrorType } from '../../../basics/error-type'; +import { findCompareToken, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { filterSameValueObjectResult } from '../../../engine/utils/value-object'; +import type { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +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 Countif extends BaseFunction { + override minParams = 2; + + override maxParams = 2; + + override calculate(range: BaseValueObject, criteria: BaseValueObject) { + if (range.isError() || criteria.isError()) { + return ErrorValueObject.create(ErrorType.NA); + } + + if (!range.isArray()) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + if (criteria.isArray()) { + return criteria.mapValue((criteriaItem) => this._handleSingleObject(range, criteriaItem)); + } + + return this._handleSingleObject(range, criteria); + } + + private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject) { + let resultArrayObject = valueObjectCompare(range, criteria); + + const [, criteriaStringObject] = findCompareToken(`${criteria.getValue()}`); + // If the condition is a numeric comparison, only numbers are counted, otherwise text is counted. + resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); + + const picked = (range as ArrayValueObject).pick(resultArrayObject as ArrayValueObject); + return this._countA(picked); + } + + private _countA(array: ArrayValueObject) { + let accumulatorAll: BaseValueObject = NumberValueObject.create(0); + array.iterator((valueObject) => { + if (valueObject == null) { + return true; // continue + } + + accumulatorAll = accumulatorAll.plusBy(1); + }); + + return accumulatorAll; + } +} diff --git a/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts new file mode 100644 index 00000000000..b0c8c8c16f5 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts @@ -0,0 +1,240 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Countifs } from '../index'; +import { ArrayValueObject, transformToValue } from '../../../../engine/value-object/array-value-object'; +import { NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ErrorType } from '../../../../basics/error-type'; + +describe('Test countifs function', () => { + const testFunction = new Countifs(FUNCTION_NAMES_STATISTICAL.COUNTIFS); + + describe('Countifs', () => { + it('Array criteria with number and string', async () => { + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4; + test1; + test12 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4; + test* + }`); + + const resultObject = testFunction.calculate(range1, criteria1); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2], [1], [0], [2]]); + }); + + it('Range and criteria, compare string', async () => { + const range = ArrayValueObject.create(`{ + a; + b; + c + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[0]]); + }); + + it('Different ranges, error reporting', async () => { + const range1 = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const criteria1 = StringValueObject.create('>2'); + + const rang2 = ArrayValueObject.create(`{ + 2; + 3; + 4; + 5 + }`); + + const criteria2 = StringValueObject.create('>3'); + const resultObject = testFunction.calculate(range1, criteria1, rang2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[ErrorType.VALUE]]); + }); + + it('Range and criteria, count number', async () => { + const range = ArrayValueObject.create(`{ + 2; + 3; + 4; + Univer + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2]]); + }); + + it('Range and array criteria', async () => { + const range = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria = ArrayValueObject.create(`{ + >2; + >3; + >4; + >5 + }`); + + const resultObject = testFunction.calculate(range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2], [1], [0], [0]]); + }); + + it('2 ranges and criteria', async () => { + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = StringValueObject.create('>2'); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = StringValueObject.create('<5'); + + const resultObject = testFunction.calculate(range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1]]); + }); + + it('2 ranges and criteria, 1 array criteria with number', async () => { + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = NumberValueObject.create(5); + + const resultObject = testFunction.calculate(range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1], [1], [0]]); + }); + + it('2 ranges and criteria, 1 array criteria with string', async () => { + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + test1; + test12; + Univer123 + }`); + + const criteria2 = StringValueObject.create('test*'); + + const resultObject = testFunction.calculate(range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1], [0], [0]]); + }); + + it('2 ranges and criteria, 2 array criteria', async () => { + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = ArrayValueObject.create(`{ + 4; + 4; + 4; + 4 + }`); + + const resultObject = testFunction.calculate(range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1], [0], [0], [0]]); + }); + + it('2 ranges and criteria, 2 array criteria, compare string', async () => { + const range1 = ArrayValueObject.create(`{ + 1; + 2; + 3; + 4; + 5; + 6 + }`); + + const criteria1 = StringValueObject.create('<5'); + + const range2 = ArrayValueObject.create(`{ + 40664; + 40665; + 40666; + 40667; + 40668; + 40669 + }`); + + const criteria2 = StringValueObject.create('<5/3/2011'); + + const resultObject = testFunction.calculate(range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[0]]); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/countifs/index.ts b/packages/engine-formula/src/functions/statistical/countifs/index.ts new file mode 100644 index 00000000000..7cd421685d3 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countifs/index.ts @@ -0,0 +1,83 @@ +/** + * 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 { ErrorType } from '../../../basics/error-type'; +import { calculateMaxDimensions, getBooleanResults, getErrorArray } from '../../../engine/utils/value-object'; +import { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +import type { BaseValueObject, IArrayValueObject } from '../../../engine/value-object/base-value-object'; +import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { NumberValueObject } from '../../../engine/value-object/primitive-object'; +import { BaseFunction } from '../../base-function'; + +export class Countifs extends BaseFunction { + override minParams = 2; + + override maxParams = 255; + + override calculate(...variants: BaseValueObject[]) { + // Range and criteria must be paired + if (variants.length % 2 !== 0) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + // Every range must be array + if (variants.some((variant, i) => i % 2 === 0 && !variant.isArray())) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); + + const errorArray = getErrorArray(variants, variants[0], maxRowLength, maxColumnLength); + + if (errorArray) { + return errorArray; + } + + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); + + return this._aggregateResults(booleanResults); + } + + private _aggregateResults(booleanResults: BaseValueObject[][]): ArrayValueObject { + const maxResults = booleanResults.map((row) => { + return row.map((booleanResult) => { + return countTrueValue(booleanResult as ArrayValueObject); + }); + }); + + const arrayValueObjectData: IArrayValueObject = { + calculateValueList: maxResults, + rowCount: maxResults.length, + columnCount: maxResults[0].length, + unitId: this.unitId || '', + sheetId: this.subUnitId || '', + row: this.row, + column: this.column, + }; + + return ArrayValueObject.create(arrayValueObjectData); + } +} + +export function countTrueValue(array: ArrayValueObject) { + let count = 0; + array.iterator((value) => { + if (value?.isBoolean() && value.getValue() === true) { + count++; + } + }); + return NumberValueObject.create(count); +} diff --git a/packages/engine-formula/src/functions/statistical/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 336fb832f2c..59142cf7e7c 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -30,15 +30,33 @@ import { Vara } from './vara'; import { Varpa } from './varpa'; import { Maxifs } from './maxifs'; import { Averagea } from './averagea'; +import { Minifs } from './minifs'; +import { Averageif } from './averageif'; +import { Averageifs } from './averageifs'; +import { Countif } from './countif'; +import { Countifs } from './countifs'; +import { Countblank } from './countblank'; +import { Mina } from './mina'; +import { Maxa } from './maxa'; +import { Avedev } from './avedev'; export const functionStatistical = [ + [Avedev, FUNCTION_NAMES_STATISTICAL.AVEDEV], [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], [Averagea, FUNCTION_NAMES_STATISTICAL.AVERAGEA], + [Averageif, FUNCTION_NAMES_STATISTICAL.AVERAGEIF], + [Averageifs, FUNCTION_NAMES_STATISTICAL.AVERAGEIFS], [Count, FUNCTION_NAMES_STATISTICAL.COUNT], + [Counta, FUNCTION_NAMES_STATISTICAL.COUNTA], + [Countblank, FUNCTION_NAMES_STATISTICAL.COUNTBLANK], + [Countif, FUNCTION_NAMES_STATISTICAL.COUNTIF], + [Countifs, FUNCTION_NAMES_STATISTICAL.COUNTIFS], [Max, FUNCTION_NAMES_STATISTICAL.MAX], + [Maxa, FUNCTION_NAMES_STATISTICAL.MAXA], + [Maxifs, FUNCTION_NAMES_STATISTICAL.MAXIFS], [Min, FUNCTION_NAMES_STATISTICAL.MIN], - [Min, FUNCTION_NAMES_STATISTICAL.MIN], - [Counta, FUNCTION_NAMES_STATISTICAL.COUNTA], + [Mina, FUNCTION_NAMES_STATISTICAL.MINA], + [Minifs, FUNCTION_NAMES_STATISTICAL.MINIFS], [StdevP, FUNCTION_NAMES_STATISTICAL.STDEV_P], [StdevS, FUNCTION_NAMES_STATISTICAL.STDEV_S], [Stdeva, FUNCTION_NAMES_STATISTICAL.STDEVA], @@ -47,5 +65,4 @@ export const functionStatistical = [ [VarS, FUNCTION_NAMES_STATISTICAL.VAR_S], [Vara, FUNCTION_NAMES_STATISTICAL.VARA], [Varpa, FUNCTION_NAMES_STATISTICAL.VARPA], - [Maxifs, FUNCTION_NAMES_STATISTICAL.MAXIFS], ]; diff --git a/packages/engine-formula/src/functions/statistical/maxa/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/maxa/__tests__/index.spec.ts new file mode 100644 index 00000000000..27126e0b9b8 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/maxa/__tests__/index.spec.ts @@ -0,0 +1,182 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Maxa } from '../index'; +import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorType } from '../../../../basics/error-type'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; + +describe('Test maxa function', () => { + const testFunction = new Maxa(FUNCTION_NAMES_STATISTICAL.MAXA); + + describe('Maxa', () => { + it('Var1 is number, var2 is number', () => { + const var1 = NumberValueObject.create(1); + const var2 = NumberValueObject.create(2); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(2); + }); + it('Var1 is number, var2 is string', () => { + const var1 = NumberValueObject.create(1); + const var2 = StringValueObject.create('test'); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.VALUE); + }); + it('Var1 is number, var2 is string number', () => { + const var1 = NumberValueObject.create(1); + const var2 = StringValueObject.create('2'); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(2); + }); + it('Var1 is number, var2 is boolean', () => { + const var1 = NumberValueObject.create(-2); + + let var2 = BooleanValueObject.create(true); + let result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + + var2 = BooleanValueObject.create(false); + result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0); + }); + it('Var1 is number, var2 is null', () => { + const var1 = NumberValueObject.create(1); + const var2 = NullValueObject.create(); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + }); + it('Var1 is number, var2 is error', () => { + const var1 = NumberValueObject.create(1); + const var2 = ErrorValueObject.create(ErrorType.NA); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.NA); + }); + + it('Var1 is number, var2 is array includes error', () => { + const var1 = NumberValueObject.create(1); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, null], + [0, ErrorType.VALUE], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.VALUE); + }); + it('Var1 is array not includes error, includes boolean value ', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [-3, null], + [false, true], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(1); + }); + it('Var1 is array not includes error, includes string ', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [3, null], + ['test', true], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(3); + }); + it('Var1 is array not includes error, includes null', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [null, null], + [null, null], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(0); + }); + + it('Var1 is number, var2 is array not includes error', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, ' ', 1.23, true, false, null], + [0, '100', '2.34', 'test', -3, null], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(100); + }); + it('Var1 is array, var2 is array', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [null], + ]), + rowCount: 1, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + ['a'], + ]), + rowCount: 1, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/maxa/index.ts b/packages/engine-formula/src/functions/statistical/maxa/index.ts new file mode 100644 index 00000000000..c2195c36958 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/maxa/index.ts @@ -0,0 +1,85 @@ +/** + * 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 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 Maxa extends BaseFunction { + override minParams = 1; + + override maxParams = 255; + + override calculate(...variants: BaseValueObject[]) { + let accumulatorAll: BaseValueObject = NumberValueObject.create(Number.NEGATIVE_INFINITY); + for (let i = 0; i < variants.length; i++) { + let variant = variants[i]; + + if (variant.isNull()) { + continue; + } + + if (variant.isString() || variant.isBoolean()) { + variant = variant.convertToNumberObjectValue(); + } + + if (variant.isError()) { + return variant as ErrorValueObject; + } + + if (variant.isArray()) { + (variant as ArrayValueObject).iterator((valueObject) => { + // Empty cells and text values in the array or reference are ignored. + if (valueObject == null || valueObject.isNull() || valueObject.isString()) { + valueObject = NumberValueObject.create(0); + } + + if (valueObject.isBoolean()) { + valueObject = valueObject.convertToNumberObjectValue(); + } + + if (valueObject.isError()) { + accumulatorAll = valueObject; + return false; // break + } + + accumulatorAll = this._validator(accumulatorAll, valueObject as BaseValueObject); + }); + } + + if (accumulatorAll.isError()) { + return accumulatorAll; + } + + accumulatorAll = this._validator(accumulatorAll, variant as BaseValueObject); + } + + if (accumulatorAll.getValue() === Number.NEGATIVE_INFINITY) { + return NumberValueObject.create(0); + } + + return accumulatorAll; + } + + private _validator(accumulatorAll: BaseValueObject, valueObject: BaseValueObject) { + const validator = accumulatorAll.isLessThan(valueObject); + if (validator.getValue()) { + accumulatorAll = valueObject; + } + return accumulatorAll; + } +} diff --git a/packages/engine-formula/src/functions/statistical/maxifs/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/maxifs/__tests__/index.spec.ts index 2c29fb709a6..572ae50bdf2 100644 --- a/packages/engine-formula/src/functions/statistical/maxifs/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/maxifs/__tests__/index.spec.ts @@ -41,6 +41,23 @@ describe('Test maxifs function', () => { expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[3]]); }); + it('Range and criteria, compare string', async () => { + const maxRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + const range = ArrayValueObject.create(`{ + a; + b; + c + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(maxRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[0]]); + }); + it('Range and array criteria', async () => { const maxRange = ArrayValueObject.create(`{ 1; diff --git a/packages/engine-formula/src/functions/statistical/maxifs/index.ts b/packages/engine-formula/src/functions/statistical/maxifs/index.ts index 6c3c060deab..25f47200d38 100644 --- a/packages/engine-formula/src/functions/statistical/maxifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/maxifs/index.ts @@ -15,8 +15,7 @@ */ import { ErrorType } from '../../../basics/error-type'; -import { expandArrayValueObject } from '../../../engine/utils/array-object'; -import { booleanObjectIntersection, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { calculateMaxDimensions, getBooleanResults, getErrorArray } from '../../../engine/utils/value-object'; import { ArrayValueObject } from '../../../engine/value-object/array-value-object'; import type { BaseValueObject, IArrayValueObject } from '../../../engine/value-object/base-value-object'; import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; @@ -46,60 +45,20 @@ export class Maxifs extends BaseFunction { return ErrorValueObject.create(ErrorType.VALUE); } - const sumRowLength = (maxRange as ArrayValueObject).getRowCount(); - const sumColumnLength = (maxRange as ArrayValueObject).getColumnCount(); - // The size of the extended range is determined by the maximum width and height of the criteria range. - 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); - } - } - }); - - const booleanResults: BaseValueObject[][] = []; - - for (let i = 0; i < variants.length; i++) { - if (i % 2 === 1) continue; - - const range = variants[i] as ArrayValueObject; - - const rangeRowLength = range.getRowCount(); - const rangeColumnLength = range.getColumnCount(); - if (rangeRowLength !== sumRowLength || rangeColumnLength !== sumColumnLength) { - return expandArrayValueObject(maxRowLength, maxColumnLength, ErrorValueObject.create(ErrorType.NA)); - } - - const criteria = variants[i + 1]; - const criteriaArray = expandArrayValueObject(maxRowLength, maxColumnLength, criteria, ErrorValueObject.create(ErrorType.NA)); - - criteriaArray.iterator((criteriaValueObject, rowIndex, columnIndex) => { - if (!criteriaValueObject) { - return; - } + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); - const resultArrayObject = valueObjectCompare(range, criteriaValueObject); + const errorArray = getErrorArray(variants, maxRange, maxRowLength, maxColumnLength); - if (booleanResults[rowIndex] === undefined) { - booleanResults[rowIndex] = []; - } + if (errorArray) { + return errorArray; + } - if (booleanResults[rowIndex][columnIndex] === undefined) { - booleanResults[rowIndex][columnIndex] = resultArrayObject; - } + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); - booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject); - }); - } + return this._aggregateResults(maxRange, booleanResults); + } + private _aggregateResults(maxRange: BaseValueObject, booleanResults: BaseValueObject[][]): ArrayValueObject { const maxResults = booleanResults.map((row) => { return row.map((booleanResult) => { const picked = (maxRange as ArrayValueObject).pick(booleanResult as ArrayValueObject); @@ -124,3 +83,4 @@ export class Maxifs extends BaseFunction { return ArrayValueObject.create(arrayValueObjectData); } } + diff --git a/packages/engine-formula/src/functions/statistical/min/index.ts b/packages/engine-formula/src/functions/statistical/min/index.ts index d9458922dc6..5b9383deb94 100644 --- a/packages/engine-formula/src/functions/statistical/min/index.ts +++ b/packages/engine-formula/src/functions/statistical/min/index.ts @@ -28,6 +28,10 @@ export class Min extends BaseFunction { for (let i = 0; i < variants.length; i++) { let variant = variants[i]; + if (variant.isNull()) { + continue; + } + if (variant.isString() || variant.isBoolean()) { variant = variant.convertToNumberObjectValue(); } @@ -40,10 +44,6 @@ export class Min extends BaseFunction { return variant as ErrorValueObject; } - if (variant.isNull()) { - continue; - } - accumulatorAll = this._validator(accumulatorAll, variant as BaseValueObject); } diff --git a/packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts new file mode 100644 index 00000000000..54a5f57cc72 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts @@ -0,0 +1,182 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Mina } from '../index'; +import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object'; +import { ErrorType } from '../../../../basics/error-type'; +import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; + +describe('Test mina function', () => { + const testFunction = new Mina(FUNCTION_NAMES_STATISTICAL.MINA); + + describe('Mina', () => { + it('Var1 is number, var2 is number', () => { + const var1 = NumberValueObject.create(1); + const var2 = NumberValueObject.create(2); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + }); + it('Var1 is number, var2 is string', () => { + const var1 = NumberValueObject.create(1); + const var2 = StringValueObject.create('test'); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.VALUE); + }); + it('Var1 is number, var2 is string number', () => { + const var1 = NumberValueObject.create(2); + const var2 = StringValueObject.create('1'); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + }); + it('Var1 is number, var2 is boolean', () => { + const var1 = NumberValueObject.create(2); + + let var2 = BooleanValueObject.create(true); + let result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + + var2 = BooleanValueObject.create(false); + result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0); + }); + it('Var1 is number, var2 is null', () => { + const var1 = NumberValueObject.create(1); + const var2 = NullValueObject.create(); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(1); + }); + it('Var1 is number, var2 is error', () => { + const var1 = NumberValueObject.create(1); + const var2 = ErrorValueObject.create(ErrorType.NA); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.NA); + }); + + it('Var1 is number, var2 is array includes error', () => { + const var1 = NumberValueObject.create(1); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, null], + [0, ErrorType.VALUE], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(ErrorType.VALUE); + }); + it('Var1 is array not includes error, includes boolean value ', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [3, null], + [false, true], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(0); + }); + it('Var1 is array not includes error, includes string ', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [3, null], + ['test', true], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(0); + }); + it('Var1 is array not includes error, includes null', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [null, null], + [null, null], + ]), + rowCount: 2, + columnCount: 2, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1); + expect(result.getValue()).toBe(0); + }); + + it('Var1 is number, var2 is array not includes error', () => { + const var1 = NumberValueObject.create(2); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [1, ' ', 1.23, true, false, null], + [0, '100', '2.34', 'test', -3, null], + ]), + rowCount: 2, + columnCount: 6, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(-3); + }); + it('Var1 is array, var2 is array', () => { + const var1 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + [null], + ]), + rowCount: 1, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const var2 = ArrayValueObject.create({ + calculateValueList: transformToValueObject([ + ['a'], + ]), + rowCount: 1, + columnCount: 1, + unitId: '', + sheetId: '', + row: 0, + column: 0, + }); + const result = testFunction.calculate(var1, var2); + expect(result.getValue()).toBe(0); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/mina/index.ts b/packages/engine-formula/src/functions/statistical/mina/index.ts new file mode 100644 index 00000000000..1fce7989e9d --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/mina/index.ts @@ -0,0 +1,85 @@ +/** + * 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 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 Mina extends BaseFunction { + override minParams = 1; + + override maxParams = 255; + + override calculate(...variants: BaseValueObject[]) { + let accumulatorAll: BaseValueObject = NumberValueObject.create(Number.POSITIVE_INFINITY); + for (let i = 0; i < variants.length; i++) { + let variant = variants[i]; + + if (variant.isNull()) { + continue; + } + + if (variant.isString() || variant.isBoolean()) { + variant = variant.convertToNumberObjectValue(); + } + + if (variant.isError()) { + return variant as ErrorValueObject; + } + + if (variant.isArray()) { + (variant as ArrayValueObject).iterator((valueObject) => { + // Empty cells and text values in the array or reference are ignored. + if (valueObject == null || valueObject.isNull() || valueObject.isString()) { + valueObject = NumberValueObject.create(0); + } + + if (valueObject.isBoolean()) { + valueObject = valueObject.convertToNumberObjectValue(); + } + + if (valueObject.isError()) { + accumulatorAll = valueObject; + return false; // break + } + + accumulatorAll = this._validator(accumulatorAll, valueObject as BaseValueObject); + }); + } + + if (accumulatorAll.isError()) { + return accumulatorAll; + } + + accumulatorAll = this._validator(accumulatorAll, variant as BaseValueObject); + } + + if (accumulatorAll.getValue() === Number.POSITIVE_INFINITY) { + return NumberValueObject.create(0); + } + + return accumulatorAll; + } + + private _validator(accumulatorAll: BaseValueObject, valueObject: BaseValueObject) { + const validator = accumulatorAll.isGreaterThan(valueObject); + if (validator.getValue()) { + accumulatorAll = valueObject; + } + return accumulatorAll; + } +} diff --git a/packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts new file mode 100644 index 00000000000..9eb69f466ad --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts @@ -0,0 +1,178 @@ +/** + * 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 { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Minifs } from '../index'; +import { ArrayValueObject, transformToValue } from '../../../../engine/value-object/array-value-object'; +import { NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; + +describe('Test minifs function', () => { + const testFunction = new Minifs(FUNCTION_NAMES_STATISTICAL.MINIFS); + + describe('Minifs', () => { + it('Range and criteria', async () => { + const minRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + const range = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(minRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2]]); + }); + + it('Range and criteria, compare string', async () => { + const minRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + const range = ArrayValueObject.create(`{ + a; + b; + c + }`); + + const criteria = StringValueObject.create('>2'); + const resultObject = testFunction.calculate(minRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[0]]); + }); + + it('Range and array criteria', async () => { + const minRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const resultObject = testFunction.calculate(minRange, range, criteria); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2], [3], [0]]); + }); + + it('2 ranges and criteria', async () => { + const minRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = StringValueObject.create('>2'); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = StringValueObject.create('<5'); + + const resultObject = testFunction.calculate(minRange, range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2]]); + }); + + it('2 ranges and criteria, 1 array criteria', async () => { + const minRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = NumberValueObject.create(5); + + const resultObject = testFunction.calculate(minRange, range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[3], [3], [0]]); + }); + + it('2 ranges and criteria, 2 array criteria', async () => { + const minRange = ArrayValueObject.create(`{ + 1; + 2; + 3 + }`); + + const range1 = ArrayValueObject.create(`{ + 2; + 3; + 4 + }`); + + const criteria1 = ArrayValueObject.create(`{ + >2; + >3; + >4 + }`); + + const range2 = ArrayValueObject.create(`{ + 3; + 4; + 5 + }`); + + const criteria2 = ArrayValueObject.create(`{ + 4; + 4; + 4; + 4 + }`); + + const resultObject = testFunction.calculate(minRange, range1, criteria1, range2, criteria2); + expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[2], [0], [0], [0]]); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/minifs/index.ts b/packages/engine-formula/src/functions/statistical/minifs/index.ts new file mode 100644 index 00000000000..876b0750ee5 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/minifs/index.ts @@ -0,0 +1,85 @@ +/** + * 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 { ErrorType } from '../../../basics/error-type'; +import { calculateMaxDimensions, getBooleanResults, getErrorArray } from '../../../engine/utils/value-object'; +import { ArrayValueObject } from '../../../engine/value-object/array-value-object'; +import type { BaseValueObject, IArrayValueObject } from '../../../engine/value-object/base-value-object'; +import { ErrorValueObject } from '../../../engine/value-object/base-value-object'; +import { BaseFunction } from '../../base-function'; + +export class Minifs extends BaseFunction { + override minParams = 3; + + override maxParams = 255; + + override calculate(minRange: BaseValueObject, ...variants: BaseValueObject[]) { + if (minRange.isError()) { + return ErrorValueObject.create(ErrorType.NA); + } + + if (!minRange.isArray()) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + // Range and criteria must be paired + if (variants.length % 2 !== 0) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + // Every range must be array + if (variants.some((variant, i) => i % 2 === 0 && !variant.isArray())) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); + + const errorArray = getErrorArray(variants, minRange, maxRowLength, maxColumnLength); + + if (errorArray) { + return errorArray; + } + + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); + + return this._aggregateResults(minRange, booleanResults); + } + + private _aggregateResults(minRange: BaseValueObject, booleanResults: BaseValueObject[][]): ArrayValueObject { + const maxResults = booleanResults.map((row) => { + return row.map((booleanResult) => { + const picked = (minRange as ArrayValueObject).pick(booleanResult as ArrayValueObject); + if (picked.getColumnCount() === 0) { + return ArrayValueObject.create('0'); + } + + return picked.min(); + }); + }); + + const arrayValueObjectData: IArrayValueObject = { + calculateValueList: maxResults, + rowCount: maxResults.length, + columnCount: maxResults[0].length, + unitId: this.unitId || '', + sheetId: this.subUnitId || '', + row: this.row, + column: this.column, + }; + + return ArrayValueObject.create(arrayValueObjectData); + } +} diff --git a/packages/sheets-formula/src/locale/function-list/statistical/en-US.ts b/packages/sheets-formula/src/locale/function-list/statistical/en-US.ts index 7543ff480b1..65ba41d6665 100644 --- a/packages/sheets-formula/src/locale/function-list/statistical/en-US.ts +++ b/packages/sheets-formula/src/locale/function-list/statistical/en-US.ts @@ -16,7 +16,7 @@ export default { AVEDEV: { - description: 'Returns the average of the absolute deviations of data points from their mean', + description: 'Returns the average of the absolute deviations of data points from their mean.', abstract: 'Returns the average of the absolute deviations of data points from their mean', links: [ { @@ -25,8 +25,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + number1: { name: 'number1', detail: 'The first number, cell reference, or range for which you want the average.' }, + number2: { name: 'number2', detail: 'Additional numbers, cell references or ranges for which you want the average, up to a maximum of 255.' }, }, }, AVERAGE: { @@ -70,7 +70,7 @@ export default { }, }, AVERAGEIF: { - description: 'Returns the average (arithmetic mean) of all the cells in a range that meet a given criteria', + description: 'Returns the average (arithmetic mean) of all the cells in a range that meet a given criteria.', abstract: 'Returns the average (arithmetic mean) of all the cells in a range that meet a given criteria', links: [ { @@ -79,12 +79,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: 'range', detail: 'One or more cells to average, including numbers or names, arrays, or references that contain numbers.' }, + criteria: { name: 'criteria', detail: 'The criteria in the form of a number, expression, cell reference, or text that defines which cells are averaged. For example, criteria can be expressed as 32, "32", ">32", "apples", or B4.' }, + averageRange: { name: 'average_range', detail: 'The actual set of cells to average. If omitted, range is used.' }, }, }, AVERAGEIFS: { - description: 'Returns the average (arithmetic mean) of all cells that meet multiple criteria', + description: 'Returns the average (arithmetic mean) of all cells that meet multiple criteria.', abstract: 'Returns the average (arithmetic mean) of all cells that meet multiple criteria', links: [ { @@ -93,8 +94,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + averageRange: { name: 'average_range', detail: 'One or more cells to average, including numbers or names, arrays, or references that contain numbers.' }, + criteriaRange1: { name: 'criteria_range1', detail: 'Is the set of cells to evaluate with the criteria.' }, + criteria1: { name: 'criteria1', detail: 'Used to define the cells for which the average will be calculated. For example, the criteria can be expressed as 32, "32", ">32", "apple", or B4' }, + criteriaRange2: { name: 'criteria_range2', detail: 'Additional ranges. You can enter up to 127 range.' }, + criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 127 criteria.' }, }, }, BETA_DIST: { @@ -321,7 +325,7 @@ export default { }, }, COUNTBLANK: { - description: 'Counts the number of blank cells within a range', + description: 'Counts the number of blank cells within a range.', abstract: 'Counts the number of blank cells within a range', links: [ { @@ -330,12 +334,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: 'range', detail: 'The range from which you want to count the blank cells.' }, }, }, COUNTIF: { - description: 'Counts the number of cells within a range that meet the given criteria', + description: 'Counts the number of cells within a range that meet the given criteria.', abstract: 'Counts the number of cells within a range that meet the given criteria', links: [ { @@ -344,12 +347,12 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: 'range', detail: 'The group of cells you want to count. Range can contain numbers, arrays, a named range, or references that contain numbers. Blank and text values are ignored.' }, + criteria: { name: 'criteria', detail: 'A number, expression, cell reference, or text string that determines which cells will be counted.\nFor example, you can use a number like 32, a comparison like ">32", a cell like B4, or a word like "apples".\nCOUNTIF uses only a single criteria. Use COUNTIFS if you want to use multiple criteria.' }, }, }, COUNTIFS: { - description: 'Counts the number of cells within a range that meet multiple criteria', + description: 'Counts the number of cells within a range that meet multiple criteria.', abstract: 'Counts the number of cells within a range that meet multiple criteria', links: [ { @@ -358,8 +361,10 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + criteriaRange1: { name: 'criteria_range1', detail: 'The first range in which to evaluate the associated criteria.' }, + criteria1: { name: 'criteria1', detail: 'The criteria in the form of a number, expression, cell reference, or text that define which cells will be counted. For example, criteria can be expressed as 32, ">32", B4, "apples", or "32".' }, + criteriaRange2: { name: 'criteria_range2', detail: 'Additional ranges. You can enter up to 127 range.' }, + criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 127 criteria.' }, }, }, COVARIANCE_P: { @@ -873,7 +878,7 @@ export default { }, }, MAXA: { - description: 'Returns the maximum value in a list of arguments, including numbers, text, and logical values', + description: 'Returns the maximum value in a list of arguments, including numbers, text, and logical values.', abstract: 'Returns the maximum value in a list of arguments, including numbers, text, and logical values', links: [ { @@ -882,12 +887,12 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: 'value1', detail: 'The first number argument for which you want to find the largest value.' }, + value2: { name: 'value2', detail: 'Number arguments 2 to 255 for which you want to find the largest value.' }, }, }, MAXIFS: { - description: 'Returns the maximum value among cells specified by a given set of conditions or criteria', + description: 'Returns the maximum value among cells specified by a given set of conditions or criteria.', abstract: 'Returns the maximum value among cells specified by a given set of conditions or criteria', links: [ { @@ -899,8 +904,8 @@ export default { maxRange: { name: 'sum_range', detail: 'The range of cells to max.' }, criteriaRange1: { name: 'criteria_range1 ', detail: 'Is the set of cells to evaluate with the criteria.' }, criteria1: { name: 'criteria1', detail: 'Is the criteria in the form of a number, expression, or text that defines which cells will be evaluated as maximum. ' }, - criteriaRange2: { name: 'criteriaRange2', detail: 'Additional ranges. You can enter up to 127 range pairs.' }, - criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 127 criteria pairs.' }, + criteriaRange2: { name: 'criteriaRange2', detail: 'Additional ranges. You can enter up to 127 ranges.' }, + criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 127 criteria.' }, }, }, MEDIAN: { @@ -947,13 +952,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: 'value1', detail: 'The first number, cell reference, or range to calculate the minimum value from.' }, + value2: { name: 'value2', detail: 'Additional numbers, cell references or ranges to calculate the minimum value from, up to a maximum of 255.' }, }, }, MINIFS: { description: 'Returns the minimum value among cells specified by a given set of conditions or criteria.', - abstract: 'Returns the minimum value among cells specified by a given set of conditions or criteria.', + abstract: 'Returns the minimum value among cells specified by a given set of conditions or criteria', links: [ { title: 'Instruction', @@ -961,8 +966,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + minRange: { name: 'min_range', detail: 'The actual range of cells in which the minimum value will be determined.' }, + criteriaRange1: { name: 'criteria_range1', detail: 'Is the set of cells to evaluate with the criteria.' }, + criteria1: { name: 'criteria1', detail: 'Is the criteria in the form of a number, expression, or text that defines which cells will be evaluated as minimum. The same set of criteria works for the MAXIFS, SUMIFS and AVERAGEIFS functions.' }, + criteriaRange2: { name: 'criteria_range2', detail: 'Additional ranges. You can enter up to 127 range.' }, + criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 127 criteria.' }, }, }, MODE_MULT: { diff --git a/packages/sheets-formula/src/locale/function-list/statistical/ja-JP.ts b/packages/sheets-formula/src/locale/function-list/statistical/ja-JP.ts index 7a06e5a9897..cfd06de8571 100644 --- a/packages/sheets-formula/src/locale/function-list/statistical/ja-JP.ts +++ b/packages/sheets-formula/src/locale/function-list/statistical/ja-JP.ts @@ -17,7 +17,7 @@ export default { AVEDEV: { description: 'データ全体の平均値に対するそれぞれのデータの絶対偏差の平均を返します。', - abstract: 'データ全体の平均値に対するそれぞれのデータの絶対偏差の平均を返します。', + abstract: 'データ全体の平均値に対するそれぞれのデータの絶対偏差の平均を返します', links: [ { title: '指導', @@ -25,13 +25,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + number1: { name: '数値 1', detail: '平均を求める 1 つ目の数値、セル参照、またはセル範囲を指定します。' }, + number2: { name: '数値 21', detail: '平均を求める追加の数値、セル参照、または範囲 (最大 255)。' }, }, }, AVERAGE: { description: '引数の平均値を返します。', - abstract: '引数の平均値を返します。', + abstract: '引数の平均値を返します', links: [ { title: '指導', @@ -39,8 +39,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + number1: { name: '数値 1', detail: '平均を求める 1 つ目の数値、セル参照、またはセル範囲を指定します。' }, + number2: { name: '数値 2', detail: '平均を求める追加の数値、セル参照、または範囲 (最大 255)。' }, }, }, AVERAGEA: { @@ -59,7 +59,7 @@ export default { }, AVERAGEIF: { description: '範囲内の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', - abstract: '範囲内の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', + abstract: '範囲内の検索条件に一致するすべてのセルの平均値 (算術平均) を返します', links: [ { title: '指導', @@ -67,13 +67,14 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '範囲', detail: '平均する 1 つまたは複数のセル (数値、または数値を含む名前、配列、セル参照) を指定します。' }, + criteria: { name: '検索条件', detail: '平均の対象となるセルを定義する条件を数値、式、セル参照、または文字列で指定します。 たとえば、検索条件は 32、"32"、">32"、"Windows"、または B4 のようになります。' }, + averageRange: { name: '平均範囲', detail: '平均する実際のセルを指定します。 何も指定しないと、範囲が使用されます。' }, }, }, AVERAGEIFS: { description: '複数の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', - abstract: '複数の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', + abstract: '複数の検索条件に一致するすべてのセルの平均値 (算術平均) を返します', links: [ { title: '指導', @@ -81,8 +82,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + averageRange: { name: '平均範囲', detail: '平均する 1 つまたは複数のセル (数値、または数値を含む名前、配列、セル参照) を指定します。' }, + criteriaRange1: { name: '条件範囲 1', detail: '条件で評価するセルのセットです。' }, + criteria1: { name: '条件 1', detail: '平均を計算するセルを定義するために使用されます。 たとえば、条件は 32、"32"、">32"、"apple"、または B4 のように表現できます。' }, + criteriaRange2: { name: '条件範囲 2', detail: '追加の範囲。 最大 127 の範囲のペアを入力できます。' }, + criteria2: { name: '条件 2', detail: '追加対応する条件です。 最大 127 条件のペアを入力できます。' }, }, }, BETA_DIST: { @@ -297,7 +301,7 @@ export default { }, COUNTBLANK: { description: '指定された範囲に含まれる空白セルの個数を返します。', - abstract: '指定された範囲に含まれる空白セルの個数を返します。', + abstract: '指定された範囲に含まれる空白セルの個数を返します', links: [ { title: '指導', @@ -305,13 +309,12 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '範囲', detail: '空白セルの個数を求めるセル範囲を指定します。' }, }, }, COUNTIF: { description: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します。', - abstract: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します。', + abstract: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します', links: [ { title: '指導', @@ -319,13 +322,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '範囲', detail: '数えるセルのグループ。 範囲には、数値、配列、名前付き範囲、(数値を含む) 参照が入ります。 空の値とテキスト値は無視されます。' }, + criteria: { name: '検索条件', detail: '個数の計算対象となるセルを決定する条件を、数値、式、セル参照、または文字列で指定します。\nたとえば、数値として 32、比較演算子として ">32"、セル参照として B4、文字列として "リンゴ" などを指定できます。\nCOUNTIF で指定できるのは、単一の検索条件のみです。 複数の検索条件を指定する場合は、COUNTIFS を使います。' }, }, }, COUNTIFS: { description: '指定された範囲に含まれるセルのうち、複数の検索条件に一致するセルの個数を返します。', - abstract: '指定された範囲に含まれるセルのうち、複数の検索条件に一致するセルの個数を返します。', + abstract: '指定された範囲に含まれるセルのうち、複数の検索条件に一致するセルの個数を返します', links: [ { title: '指導', @@ -333,8 +336,10 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + criteriaRange1: { name: '条件範囲 1', detail: '対応する条件による評価の対象となる最初の範囲を指定します。' }, + criteria1: { name: '検索条件 1', detail: '計算の対象となるセルを定義する条件を数値、式、セル参照、または文字列で指定します。 たとえば、条件は 32、">32"、B4、"Windows"、または "32" のようになります。' }, + criteriaRange2: { name: '条件範囲 2', detail: '追加の範囲。 最大 127 の範囲のペアを入力できます。' }, + criteria2: { name: '条件 2', detail: '追加対応する条件です。 最大 127 条件のペアを入力できます。' }, }, }, COVARIANCE_P: { @@ -829,7 +834,7 @@ export default { }, MAX: { description: '引数リストに含まれる最大の数値を返します。', - abstract: '引数リストに含まれる最大の数値を返します。', + abstract: '引数リストに含まれる最大の数値を返します', links: [ { title: '指導', @@ -837,13 +842,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '数値 1', detail: '最大の値を見つけるため、最初の数値引数を指定します。' }, + value2: { name: '数値 2', detail: '最大の値を見つけるため、2 ~ 255 個までの数値引数を指定します。' }, }, }, MAXA: { description: '数値、文字列、および論理値を含む引数リストから最大の数値を返します。', - abstract: '数値、文字列、および論理値を含む引数リストから最大の数値を返します。', + abstract: '数値、文字列、および論理値を含む引数リストから最大の数値を返します', links: [ { title: '指導', @@ -851,8 +856,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '値 1', detail: '最大の値を見つけるため、最初の数値引数を指定します。' }, + value2: { name: '値 2', detail: '最大の値を見つけるため、2 ~ 255 個までの数値引数を指定します。' }, }, }, MAXIFS: { @@ -896,8 +901,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + number1: { name: 'number1', detail: '最小値を計算する最初の数値、セル参照、またはセル範囲。' }, + number2: { name: 'number2', detail: '最小値を計算するために、最大 255 個の追加の数値、セル参照、またはセル範囲を含めることができます。' }, }, }, MINA: { @@ -910,13 +915,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '値 1', detail: '最小値を計算する最初の数値、セル参照、またはセル範囲。' }, + value2: { name: '値 2', detail: '最小値を計算するために、最大 255 個の追加の数値、セル参照、またはセル範囲を含めることができます。' }, }, }, MINIFS: { description: '条件セットで指定されたセルの中の最小値を返します。', - abstract: '条件セットで指定されたセルの中の最小値を返します。', + abstract: '条件セットで指定されたセルの中の最小値を返します', links: [ { title: '指導', @@ -924,8 +929,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + minRange: { name: '最少範囲', detail: '最小値を求めるセルの実際の範囲です。' }, + criteriaRange1: { name: '条件範囲 1', detail: '条件で評価するセルのセットです。' }, + criteria1: { name: '条件 1', detail: '最小として評価されるセルを定義する、数値、式、またはテキストの形式での条件です。 同じ条件セットを、MAXIFS、SUMIFS、および AVERAGEIFS 関数に対して使用できます。' }, + criteriaRange2: { name: '条件範囲 2', detail: '追加の範囲。 最大 127 の範囲のペアを入力できます。' }, + criteria2: { name: '条件 2', detail: '追加対応する条件です。 最大 127 条件のペアを入力できます。' }, }, }, MODE_MULT: { diff --git a/packages/sheets-formula/src/locale/function-list/statistical/zh-CN.ts b/packages/sheets-formula/src/locale/function-list/statistical/zh-CN.ts index 568bc830ba9..fd0911b52f7 100644 --- a/packages/sheets-formula/src/locale/function-list/statistical/zh-CN.ts +++ b/packages/sheets-formula/src/locale/function-list/statistical/zh-CN.ts @@ -16,7 +16,7 @@ export default { AVEDEV: { - description: '返回数据点与它们的平均值的绝对偏差平均值', + description: '返回数据点与它们的平均值的绝对偏差平均值。', abstract: '返回数据点与它们的平均值的绝对偏差平均值', links: [ { @@ -25,8 +25,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + number1: { name: '数值 1', detail: '要计算平均值的第一个数字、单元格引用或单元格区域。' }, + number2: { name: '数值 2', detail: '要计算平均值的其他数字、单元格引用或单元格区域,最多可包含 255 个。' }, }, }, AVERAGE: { @@ -70,7 +70,7 @@ export default { }, }, AVERAGEIF: { - description: '返回区域中满足给定条件的所有单元格的平均值(算术平均值)', + description: '返回区域中满足给定条件的所有单元格的平均值(算术平均值)。', abstract: '返回区域中满足给定条件的所有单元格的平均值(算术平均值)', links: [ { @@ -79,12 +79,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '范围', detail: '要计算平均值的一个或多个单元格,其中包含数字或包含数字的名称、数组或引用。' }, + criteria: { name: '条件', detail: '形式为数字、表达式、单元格引用或文本的条件,用来定义将计算平均值的单元格。 例如,条件可以表示为 32、"32"、">32"、"苹果" 或 B4。' }, + averageRange: { name: '平均范围', detail: '计算平均值的实际单元格组。 如果省略,则使用 range。' }, }, }, AVERAGEIFS: { - description: '返回满足多个条件的所有单元格的平均值(算术平均值)', + description: '返回满足多个条件的所有单元格的平均值(算术平均值)。', abstract: '返回满足多个条件的所有单元格的平均值(算术平均值)', links: [ { @@ -93,8 +94,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + averageRange: { name: '平均值范围', detail: '要计算平均值的一个或多个单元格,其中包含数字或包含数字的名称、数组或引用。' }, + criteriaRange1: { name: '条件范围 1', detail: '是一组用于条件计算的单元格。' }, + criteria1: { name: '条件 1', detail: '用来定义将计算平均值的单元格。 例如,条件可以表示为 32、"32"、">32"、"苹果" 或 B4' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, BETA_DIST: { @@ -321,7 +325,7 @@ export default { }, }, COUNTBLANK: { - description: '计算区域内空白单元格的数量', + description: '计算区域内空白单元格的数量。', abstract: '计算区域内空白单元格的数量', links: [ { @@ -330,12 +334,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '范围', detail: '需要计算其中空白单元格个数的区域。' }, }, }, COUNTIF: { - description: '计算区域内符合给定条件的单元格的数量', + description: '计算区域内符合给定条件的单元格的数量。', abstract: '计算区域内符合给定条件的单元格的数量', links: [ { @@ -344,12 +347,12 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '范围', detail: '要进行计数的单元格组。 区域可以包括数字、数组、命名区域或包含数字的引用。 空白和文本值将被忽略。' }, + criteria: { name: '条件', detail: '用于决定要统计哪些单元格的数量的数字、表达式、单元格引用或文本字符串。\n例如,可以使用 32 之类数字,“>32”之类比较,B4 之类单元格,或“苹果”之类单词。\nCOUNTIF 仅使用一个条件。 如果要使用多个条件,请使用 COUNTIFS。' }, }, }, COUNTIFS: { - description: '计算区域内符合多个条件的单元格的数量', + description: '计算区域内符合多个条件的单元格的数量。', abstract: '计算区域内符合多个条件的单元格的数量', links: [ { @@ -358,8 +361,10 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + criteriaRange1: { name: '条件范围 1', detail: '在其中计算关联条件的第一个区域。' }, + criteria1: { name: '条件 1', detail: '条件的形式为数字、表达式、单元格引用或文本,它定义了要计数的单元格范围。 例如,条件可以表示为 32、">32"、B4、"apples"或 "32"。' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, COVARIANCE_P: { @@ -873,7 +878,7 @@ export default { }, }, MAXA: { - description: '返回参数列表中的最大值,包括数字、文本和逻辑值', + description: '返回参数列表中的最大值,包括数字、文本和逻辑值。', abstract: '返回参数列表中的最大值,包括数字、文本和逻辑值', links: [ { @@ -882,8 +887,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '值 1', detail: '要从中找出最大值的第一个数值参数。' }, + value2: { name: '值 2', detail: '要从中找出最大值的 2 到 255 个数值参数。' }, }, }, MAXIFS: { @@ -897,10 +902,10 @@ export default { ], functionParameter: { maxRange: { name: '最大值范围', detail: '确定最大值的实际单元格区域。' }, - criteriaRange1: { name: '条件范围 1', detail: '条件1 一组用于条件计算的单元格。' }, - criteria1: { name: '条件 1', detail: '条件 1 用于确定哪些单元格是最大值的条件,格式为数字、表达式或文本。' }, - criteriaRange2: { name: '条件范围 2', detail: '附加区域及其关联条件。 最多可以输入 127 个区域/条件对。' }, - criteria2: { name: '条件 2', detail: '附加区域及其关联条件。 最多可以输入 127 个区域/条件对。' }, + criteriaRange1: { name: '条件范围 1', detail: '是一组用于条件计算的单元格。' }, + criteria1: { name: '条件 1', detail: '用于确定哪些单元格是最大值的条件,格式为数字、表达式或文本。 一组相同的条件适用于 MINIFS、SUMIFS 和 AVERAGEIFS 函数。' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, MEDIAN: { @@ -938,7 +943,7 @@ export default { }, }, MINA: { - description: '返回参数列表中的最小值,包括数字、文本和逻辑值', + description: '返回参数列表中的最小值,包括数字、文本和逻辑值。', abstract: '返回参数列表中的最小值,包括数字、文本和逻辑值', links: [ { @@ -947,13 +952,13 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '值 1', detail: '要计算最小值的第一个数字、单元格引用或单元格区域。' }, + value2: { name: '值 2', detail: '要计算最小值的其他数字、单元格引用或单元格区域,最多可包含 255 个。' }, }, }, MINIFS: { description: '返回一组给定条件或标准指定的单元格之间的最小值。', - abstract: '返回一组给定条件或标准指定的单元格之间的最小值。', + abstract: '返回一组给定条件或标准指定的单元格之间的最小值', links: [ { title: '教学', @@ -961,8 +966,11 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + minRange: { name: '最小值范围', detail: '确定最小值的实际单元格区域。' }, + criteriaRange1: { name: '条件范围 1', detail: '是一组用于条件计算的单元格。' }, + criteria1: { name: '条件 1', detail: '用于确定哪些单元格是最小值的条件,格式为数字、表达式或文本。 一组相同的条件适用于 MAXIFS、SUMIFS 和 AVERAGEIFS 函数。' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, MODE_MULT: { diff --git a/packages/sheets-formula/src/services/function-list/math.ts b/packages/sheets-formula/src/services/function-list/math.ts index 9353a2954b2..5a768acd3b9 100644 --- a/packages/sheets-formula/src/services/function-list/math.ts +++ b/packages/sheets-formula/src/services/function-list/math.ts @@ -1649,7 +1649,7 @@ export const FUNCTION_LIST_MATH: IFunctionInfo[] = [ { name: 'formula.functionList.SUMIFS.functionParameter.criteria2.name', detail: 'formula.functionList.SUMIFS.functionParameter.criteria2.detail', - example: '"<100"', + example: '"<20"', require: 0, repeat: 1, }, diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index 24d62ec56d3..7ab5a994734 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -34,9 +34,9 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ { name: 'formula.functionList.AVEDEV.functionParameter.number2.name', detail: 'formula.functionList.AVEDEV.functionParameter.number2.detail', - example: 'A1:A20', - require: 1, - repeat: 0, + example: 'B1:B20', + require: 0, + repeat: 1, }, ], }, @@ -92,19 +92,26 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.AVERAGEIF.abstract', functionParameter: [ { - name: 'formula.functionList.AVERAGEIF.functionParameter.number1.name', - detail: 'formula.functionList.AVERAGEIF.functionParameter.number1.detail', + name: 'formula.functionList.AVERAGEIF.functionParameter.range.name', + detail: 'formula.functionList.AVERAGEIF.functionParameter.range.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.AVERAGEIF.functionParameter.number2.name', - detail: 'formula.functionList.AVERAGEIF.functionParameter.number2.detail', - example: 'A1:A20', + name: 'formula.functionList.AVERAGEIF.functionParameter.criteria.name', + detail: 'formula.functionList.AVERAGEIF.functionParameter.criteria.detail', + example: '">5"', require: 1, repeat: 0, }, + { + name: 'formula.functionList.AVERAGEIF.functionParameter.averageRange.name', + detail: 'formula.functionList.AVERAGEIF.functionParameter.averageRange.detail', + example: 'B1:B20', + require: 0, + repeat: 0, + }, ], }, { @@ -114,19 +121,40 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.AVERAGEIFS.abstract', functionParameter: [ { - name: 'formula.functionList.AVERAGEIFS.functionParameter.number1.name', - detail: 'formula.functionList.AVERAGEIFS.functionParameter.number1.detail', + name: 'formula.functionList.AVERAGEIFS.functionParameter.averageRange.name', + detail: 'formula.functionList.AVERAGEIFS.functionParameter.averageRange.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.AVERAGEIFS.functionParameter.number2.name', - detail: 'formula.functionList.AVERAGEIFS.functionParameter.number2.detail', - example: 'A1:A20', + name: 'formula.functionList.AVERAGEIFS.functionParameter.criteriaRange1.name', + detail: 'formula.functionList.AVERAGEIFS.functionParameter.criteriaRange1.detail', + example: 'B1:B20', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.AVERAGEIFS.functionParameter.criteria1.name', + detail: 'formula.functionList.AVERAGEIFS.functionParameter.criteria1.detail', + example: '">10"', require: 1, repeat: 0, }, + { + name: 'formula.functionList.AVERAGEIFS.functionParameter.criteriaRange2.name', + detail: 'formula.functionList.AVERAGEIFS.functionParameter.criteriaRange2.detail', + example: 'C1:C20', + require: 0, + repeat: 1, + }, + { + name: 'formula.functionList.AVERAGEIFS.functionParameter.criteria2.name', + detail: 'formula.functionList.AVERAGEIFS.functionParameter.criteria2.detail', + example: '"<20"', + require: 0, + repeat: 1, + }, ], }, { @@ -468,15 +496,8 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.COUNTBLANK.abstract', functionParameter: [ { - name: 'formula.functionList.COUNTBLANK.functionParameter.number1.name', - detail: 'formula.functionList.COUNTBLANK.functionParameter.number1.detail', - example: 'A1:A20', - require: 1, - repeat: 0, - }, - { - name: 'formula.functionList.COUNTBLANK.functionParameter.number2.name', - detail: 'formula.functionList.COUNTBLANK.functionParameter.number2.detail', + name: 'formula.functionList.COUNTBLANK.functionParameter.range.name', + detail: 'formula.functionList.COUNTBLANK.functionParameter.range.detail', example: 'A1:A20', require: 1, repeat: 0, @@ -490,16 +511,16 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.COUNTIF.abstract', functionParameter: [ { - name: 'formula.functionList.COUNTIF.functionParameter.number1.name', - detail: 'formula.functionList.COUNTIF.functionParameter.number1.detail', + name: 'formula.functionList.COUNTIF.functionParameter.range.name', + detail: 'formula.functionList.COUNTIF.functionParameter.range.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.COUNTIF.functionParameter.number2.name', - detail: 'formula.functionList.COUNTIF.functionParameter.number2.detail', - example: 'A1:A20', + name: 'formula.functionList.COUNTIF.functionParameter.criteria.name', + detail: 'formula.functionList.COUNTIF.functionParameter.criteria.detail', + example: '">5"', require: 1, repeat: 0, }, @@ -512,19 +533,33 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.COUNTIFS.abstract', functionParameter: [ { - name: 'formula.functionList.COUNTIFS.functionParameter.number1.name', - detail: 'formula.functionList.COUNTIFS.functionParameter.number1.detail', + name: 'formula.functionList.COUNTIFS.functionParameter.criteriaRange1.name', + detail: 'formula.functionList.COUNTIFS.functionParameter.criteriaRange1.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.COUNTIFS.functionParameter.number2.name', - detail: 'formula.functionList.COUNTIFS.functionParameter.number2.detail', - example: 'A1:A20', + name: 'formula.functionList.COUNTIFS.functionParameter.criteria1.name', + detail: 'formula.functionList.COUNTIFS.functionParameter.criteria1.detail', + example: '">10"', require: 1, repeat: 0, }, + { + name: 'formula.functionList.COUNTIFS.functionParameter.criteriaRange2.name', + detail: 'formula.functionList.COUNTIFS.functionParameter.criteriaRange2.detail', + example: 'B1:B20', + require: 0, + repeat: 1, + }, + { + name: 'formula.functionList.COUNTIFS.functionParameter.criteria2.name', + detail: 'formula.functionList.COUNTIFS.functionParameter.criteria2.detail', + example: '"<20"', + require: 0, + repeat: 1, + }, ], }, { @@ -1327,18 +1362,18 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.MAXA.abstract', functionParameter: [ { - name: 'formula.functionList.MAXA.functionParameter.number1.name', - detail: 'formula.functionList.MAXA.functionParameter.number1.detail', + name: 'formula.functionList.MAXA.functionParameter.value1.name', + detail: 'formula.functionList.MAXA.functionParameter.value1.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.MAXA.functionParameter.number2.name', - detail: 'formula.functionList.MAXA.functionParameter.number2.detail', - example: 'A1:A20', - require: 1, - repeat: 0, + name: 'formula.functionList.MAXA.functionParameter.value2.name', + detail: 'formula.functionList.MAXA.functionParameter.value2.detail', + example: 'B1:B20', + require: 0, + repeat: 1, }, ], }, @@ -1358,7 +1393,7 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ { name: 'formula.functionList.MAXIFS.functionParameter.criteriaRange1.name', detail: 'formula.functionList.MAXIFS.functionParameter.criteriaRange1.detail', - example: 'A1:A20', + example: 'B1:B20', require: 1, repeat: 0, }, @@ -1372,14 +1407,14 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ { name: 'formula.functionList.MAXIFS.functionParameter.criteriaRange2.name', detail: 'formula.functionList.MAXIFS.functionParameter.criteriaRange2.detail', - example: 'A1:A20', + example: 'C1:C20', require: 0, repeat: 1, }, { name: 'formula.functionList.MAXIFS.functionParameter.criteria2.name', detail: 'formula.functionList.MAXIFS.functionParameter.criteria2.detail', - example: '"<100"', + example: '"<20"', require: 0, repeat: 1, }, @@ -1437,18 +1472,18 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.MINA.abstract', functionParameter: [ { - name: 'formula.functionList.MINA.functionParameter.number1.name', - detail: 'formula.functionList.MINA.functionParameter.number1.detail', + name: 'formula.functionList.MINA.functionParameter.value1.name', + detail: 'formula.functionList.MINA.functionParameter.value1.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.MINA.functionParameter.number2.name', - detail: 'formula.functionList.MINA.functionParameter.number2.detail', - example: 'A1:A20', - require: 1, - repeat: 0, + name: 'formula.functionList.MINA.functionParameter.value2.name', + detail: 'formula.functionList.MINA.functionParameter.value2.detail', + example: 'B1:B20', + require: 0, + repeat: 1, }, ], }, @@ -1459,19 +1494,40 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ abstract: 'formula.functionList.MINIFS.abstract', functionParameter: [ { - name: 'formula.functionList.MINIFS.functionParameter.number1.name', - detail: 'formula.functionList.MINIFS.functionParameter.number1.detail', + name: 'formula.functionList.MINIFS.functionParameter.minRange.name', + detail: 'formula.functionList.MINIFS.functionParameter.minRange.detail', example: 'A1:A20', require: 1, repeat: 0, }, { - name: 'formula.functionList.MINIFS.functionParameter.number2.name', - detail: 'formula.functionList.MINIFS.functionParameter.number2.detail', - example: 'A1:A20', + name: 'formula.functionList.MINIFS.functionParameter.criteriaRange1.name', + detail: 'formula.functionList.MINIFS.functionParameter.criteriaRange1.detail', + example: 'B1:B20', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.MINIFS.functionParameter.criteria1.name', + detail: 'formula.functionList.MINIFS.functionParameter.criteria1.detail', + example: '">10"', require: 1, repeat: 0, }, + { + name: 'formula.functionList.MINIFS.functionParameter.criteriaRange2.name', + detail: 'formula.functionList.MINIFS.functionParameter.criteriaRange2.detail', + example: 'C1:C20', + require: 0, + repeat: 1, + }, + { + name: 'formula.functionList.MINIFS.functionParameter.criteria2.name', + detail: 'formula.functionList.MINIFS.functionParameter.criteria2.detail', + example: '"<20"', + require: 0, + repeat: 1, + }, ], }, {