From a06f7e752202a26c12085430ce314fb88367ab54 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Wed, 19 Jun 2024 20:24:37 +0800 Subject: [PATCH 01/18] feat(formula): minifs --- .../src/functions/math/sumifs/index.ts | 4 + .../src/functions/statistical/function-map.ts | 2 + .../src/functions/statistical/maxifs/index.ts | 4 + .../minifs/__tests__/index.spec.ts | 161 ++++++++++++++++++ .../src/functions/statistical/minifs/index.ts | 130 ++++++++++++++ .../locale/function-list/statistical/en-US.ts | 15 +- .../locale/function-list/statistical/ja-JP.ts | 9 +- .../locale/function-list/statistical/zh-CN.ts | 17 +- .../src/services/function-list/statistical.ts | 31 +++- 9 files changed, 352 insertions(+), 21 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/minifs/index.ts diff --git a/packages/engine-formula/src/functions/math/sumifs/index.ts b/packages/engine-formula/src/functions/math/sumifs/index.ts index d6e6a7f7a68..cc17a5c0f07 100644 --- a/packages/engine-formula/src/functions/math/sumifs/index.ts +++ b/packages/engine-formula/src/functions/math/sumifs/index.ts @@ -101,6 +101,10 @@ export class Sumifs extends BaseFunction { }); } + 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/statistical/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 336fb832f2c..09e1c01b184 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -30,6 +30,7 @@ import { Vara } from './vara'; import { Varpa } from './varpa'; import { Maxifs } from './maxifs'; import { Averagea } from './averagea'; +import { Minifs } from './minifs'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], @@ -48,4 +49,5 @@ export const functionStatistical = [ [Vara, FUNCTION_NAMES_STATISTICAL.VARA], [Varpa, FUNCTION_NAMES_STATISTICAL.VARPA], [Maxifs, FUNCTION_NAMES_STATISTICAL.MAXIFS], + [Minifs, FUNCTION_NAMES_STATISTICAL.MINIFS], ]; diff --git a/packages/engine-formula/src/functions/statistical/maxifs/index.ts b/packages/engine-formula/src/functions/statistical/maxifs/index.ts index 6c3c060deab..13e829d14ca 100644 --- a/packages/engine-formula/src/functions/statistical/maxifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/maxifs/index.ts @@ -100,6 +100,10 @@ export class Maxifs extends BaseFunction { }); } + 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); 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..905f591ff0d --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts @@ -0,0 +1,161 @@ +/** + * 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 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..1e850bb596b --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/minifs/index.ts @@ -0,0 +1,130 @@ +/** + * 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 { expandArrayValueObject } from '../../../engine/utils/array-object'; +import { booleanObjectIntersection, valueObjectCompare } from '../../../engine/utils/object-compare'; +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 sumRowLength = (minRange as ArrayValueObject).getRowCount(); + const sumColumnLength = (minRange 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 resultArrayObject = valueObjectCompare(range, criteriaValueObject); + + if (booleanResults[rowIndex] === undefined) { + booleanResults[rowIndex] = []; + } + + if (booleanResults[rowIndex][columnIndex] === undefined) { + booleanResults[rowIndex][columnIndex] = resultArrayObject; + } + + booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject); + }); + } + + 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..04e647e1237 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 @@ -887,7 +887,7 @@ export default { }, }, 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 +899,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: { @@ -953,7 +953,7 @@ export default { }, 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 +961,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 126 range.' }, + criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 126 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..3ab2513faa1 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 @@ -916,7 +916,7 @@ export default { }, MINIFS: { description: '条件セットで指定されたセルの中の最小値を返します。', - abstract: '条件セットで指定されたセルの中の最小値を返します。', + abstract: '条件セットで指定されたセルの中の最小値を返します', links: [ { title: '指導', @@ -924,8 +924,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: '追加の範囲と対応する条件です。 最大 126 個の範囲/条件ペアを入力できます。' }, + criteria2: { name: '条件 2', detail: '追加の範囲と対応する条件です。 最大 126 個の範囲/条件ペアを入力できます。' }, }, }, 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..2224b8519e4 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 @@ -897,10 +897,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: '附加区域。 最多可以输入 126 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 126 个条件。' }, }, }, MEDIAN: { @@ -953,7 +953,7 @@ export default { }, MINIFS: { description: '返回一组给定条件或标准指定的单元格之间的最小值。', - abstract: '返回一组给定条件或标准指定的单元格之间的最小值。', + abstract: '返回一组给定条件或标准指定的单元格之间的最小值', links: [ { title: '教学', @@ -961,8 +961,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: '附加区域。 最多可以输入 126 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 126 个条件。' }, }, }, MODE_MULT: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index 24d62ec56d3..ba40dda11be 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -1459,19 +1459,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: '"<100"', + require: 1, + repeat: 0, + }, + { + name: 'formula.functionList.MINIFS.functionParameter.criteriaRange2.name', + detail: 'formula.functionList.MINIFS.functionParameter.criteriaRange2.detail', + example: 'B1:B20', + require: 0, + repeat: 1, + }, + { + name: 'formula.functionList.MINIFS.functionParameter.criteria2.name', + detail: 'formula.functionList.MINIFS.functionParameter.criteria2.detail', + example: 'B1:B20', + require: 0, + repeat: 1, + }, ], }, { From 27c88d391ed260af44d41b849f30d24b3b001a77 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Wed, 19 Jun 2024 20:53:32 +0800 Subject: [PATCH 02/18] fix(formula): common util for xxxIFS function --- .../src/engine/utils/value-object.ts | 25 +++++++++++++++++++ .../src/functions/math/sumifs/index.ts | 19 +++----------- .../src/functions/statistical/maxifs/index.ts | 20 +++------------ .../src/functions/statistical/minifs/index.ts | 25 +++++-------------- 4 files changed, 38 insertions(+), 51 deletions(-) diff --git a/packages/engine-formula/src/engine/utils/value-object.ts b/packages/engine-formula/src/engine/utils/value-object.ts index 5d9b018320a..0610cb43fa2 100644 --- a/packages/engine-formula/src/engine/utils/value-object.ts +++ b/packages/engine-formula/src/engine/utils/value-object.ts @@ -129,3 +129,28 @@ 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 }; +} diff --git a/packages/engine-formula/src/functions/math/sumifs/index.ts b/packages/engine-formula/src/functions/math/sumifs/index.ts index cc17a5c0f07..eeec1e1cf3f 100644 --- a/packages/engine-formula/src/functions/math/sumifs/index.ts +++ b/packages/engine-formula/src/functions/math/sumifs/index.ts @@ -17,6 +17,7 @@ import { ErrorType } from '../../../basics/error-type'; import { expandArrayValueObject } from '../../../engine/utils/array-object'; import { booleanObjectIntersection, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { calculateMaxDimensions } 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,24 +47,10 @@ export class Sumifs extends BaseFunction { return ErrorValueObject.create(ErrorType.VALUE); } + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); + 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; - - 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[][] = []; diff --git a/packages/engine-formula/src/functions/statistical/maxifs/index.ts b/packages/engine-formula/src/functions/statistical/maxifs/index.ts index 13e829d14ca..4d22ee8e302 100644 --- a/packages/engine-formula/src/functions/statistical/maxifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/maxifs/index.ts @@ -17,6 +17,7 @@ import { ErrorType } from '../../../basics/error-type'; import { expandArrayValueObject } from '../../../engine/utils/array-object'; import { booleanObjectIntersection, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { calculateMaxDimensions } 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,24 +47,10 @@ export class Maxifs extends BaseFunction { return ErrorValueObject.create(ErrorType.VALUE); } + const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); + 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[][] = []; @@ -128,3 +115,4 @@ export class Maxifs extends BaseFunction { return ArrayValueObject.create(arrayValueObjectData); } } + diff --git a/packages/engine-formula/src/functions/statistical/minifs/index.ts b/packages/engine-formula/src/functions/statistical/minifs/index.ts index 1e850bb596b..d8ae37bb12b 100644 --- a/packages/engine-formula/src/functions/statistical/minifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/minifs/index.ts @@ -17,6 +17,7 @@ import { ErrorType } from '../../../basics/error-type'; import { expandArrayValueObject } from '../../../engine/utils/array-object'; import { booleanObjectIntersection, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { calculateMaxDimensions } 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,24 +47,10 @@ export class Minifs extends BaseFunction { return ErrorValueObject.create(ErrorType.VALUE); } - const sumRowLength = (minRange as ArrayValueObject).getRowCount(); - const sumColumnLength = (minRange 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 { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); + + const minRowLength = (minRange as ArrayValueObject).getRowCount(); + const minColumnLength = (minRange as ArrayValueObject).getColumnCount(); const booleanResults: BaseValueObject[][] = []; @@ -74,7 +61,7 @@ export class Minifs extends BaseFunction { const rangeRowLength = range.getRowCount(); const rangeColumnLength = range.getColumnCount(); - if (rangeRowLength !== sumRowLength || rangeColumnLength !== sumColumnLength) { + if (rangeRowLength !== minRowLength || rangeColumnLength !== minColumnLength) { return expandArrayValueObject(maxRowLength, maxColumnLength, ErrorValueObject.create(ErrorType.NA)); } From afe5c435b2d835ada20e8531b8cdffe26c63c50d Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Thu, 20 Jun 2024 15:45:53 +0800 Subject: [PATCH 03/18] feat(formula): add averageif function --- .../averageif/__tests__/index.spec.ts | 82 +++++++++++++++++++ .../functions/statistical/averageif/index.ts | 64 +++++++++++++++ .../src/functions/statistical/function-map.ts | 2 + .../locale/function-list/statistical/en-US.ts | 7 +- .../locale/function-list/statistical/ja-JP.ts | 7 +- .../locale/function-list/statistical/zh-CN.ts | 7 +- .../src/services/function-list/statistical.ts | 17 ++-- 7 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/averageif/index.ts 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..cf8282d2ada --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts @@ -0,0 +1,82 @@ +/** + * 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 } from '../../../../engine/value-object/array-value-object'; +import { FUNCTION_NAMES_STATISTICAL } from '../../function-names'; +import { Averageif } from '../index'; +import { StringValueObject } from '../../../../engine/value-object/primitive-object'; + +describe('Test averageif function', () => { + const testFunction = new Averageif(FUNCTION_NAMES_STATISTICAL.AVERAGEIF); + + describe('Averageif', () => { + it('Range and criteria', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + 1; + 4; + 44; + 444 + }`); + + const criteria = StringValueObject.create('>40'); + + const resultObject = testFunction.calculate(range, criteria); + expect(resultObject.getValue()).toBe(244); + }); + + it('Average range with wildcard asterisk', async () => { + const range = ArrayValueObject.create(/*ts*/ `{ + Ada; + test1; + test12; + Univer + }`); + + const criteria = StringValueObject.create('test*'); + + const sumRange = ArrayValueObject.create(/*ts*/ `{ + 1; + 1; + 1; + 1 + }`); + + const resultObject = testFunction.calculate(range, criteria, sumRange); + 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([[4], [4], [44], [444]]); + }); + }); +}); 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..d8b2496f8d5 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageif/index.ts @@ -0,0 +1,64 @@ +/** + * 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 { valueObjectCompare } from '../../../engine/utils/object-compare'; +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 calculate(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { + if (range.isError() || criteria.isError() || averageRange?.isError()) { + return ErrorValueObject.create(ErrorType.NA); + } + + if (!range.isArray() || (averageRange && !averageRange.isArray())) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + if (criteria.isArray()) { + return criteria.map((criteriaItem) => this._handleSingleObject(range, criteriaItem, averageRange)); + } + + return this._handleSingleObject(range, criteria, averageRange); + } + + private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { + const resultArrayObject = valueObjectCompare(range, criteria); + + // averageRange has the same dimensions as range + const averageRangeArray = averageRange + ? (averageRange as ArrayValueObject).slice( + [0, (range as ArrayValueObject).getRowCount()], + [0, (range as ArrayValueObject).getColumnCount()] + ) + : (range as ArrayValueObject); + + if (!averageRangeArray) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + const picked = averageRangeArray.pick(resultArrayObject as ArrayValueObject); + const sum = picked.sum(); + const count = picked.count(); + return sum.divided(count); + } +} diff --git a/packages/engine-formula/src/functions/statistical/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 09e1c01b184..0bc808257ba 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -31,10 +31,12 @@ import { Varpa } from './varpa'; import { Maxifs } from './maxifs'; import { Averagea } from './averagea'; import { Minifs } from './minifs'; +import { Averageif } from './averageif'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], [Averagea, FUNCTION_NAMES_STATISTICAL.AVERAGEA], + [Averageif, FUNCTION_NAMES_STATISTICAL.AVERAGEIF], [Count, FUNCTION_NAMES_STATISTICAL.COUNT], [Max, FUNCTION_NAMES_STATISTICAL.MAX], [Min, FUNCTION_NAMES_STATISTICAL.MIN], 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 04e647e1237..4098a82c293 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 @@ -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,8 +79,9 @@ 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: { 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 3ab2513faa1..eccc4dca340 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 @@ -59,7 +59,7 @@ export default { }, AVERAGEIF: { description: '範囲内の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', - abstract: '範囲内の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', + abstract: '範囲内の検索条件に一致するすべてのセルの平均値 (算術平均) を返します', links: [ { title: '指導', @@ -67,8 +67,9 @@ 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: { 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 2224b8519e4..c3334a73712 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 @@ -70,7 +70,7 @@ export default { }, }, AVERAGEIF: { - description: '返回区域中满足给定条件的所有单元格的平均值(算术平均值)', + description: '返回区域中满足给定条件的所有单元格的平均值(算术平均值)。', abstract: '返回区域中满足给定条件的所有单元格的平均值(算术平均值)', links: [ { @@ -79,8 +79,9 @@ 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: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index ba40dda11be..6e144c95425 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -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, + }, ], }, { From 7ef41528b777a4c46b38b29fde605c5c0bc066f9 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Thu, 20 Jun 2024 16:58:06 +0800 Subject: [PATCH 04/18] feat(formula): add averageifs --- .../src/engine/utils/value-object.ts | 58 ++++++- .../src/functions/math/sumifs/index.ts | 45 +---- .../averageifs/__tests__/index.spec.ts | 162 ++++++++++++++++++ .../functions/statistical/averageifs/index.ts | 83 +++++++++ .../src/functions/statistical/function-map.ts | 2 + .../src/functions/statistical/maxifs/index.ts | 44 +---- .../src/functions/statistical/minifs/index.ts | 44 +---- .../locale/function-list/statistical/en-US.ts | 9 +- .../locale/function-list/statistical/ja-JP.ts | 9 +- .../locale/function-list/statistical/zh-CN.ts | 9 +- .../src/services/function-list/math.ts | 2 +- .../src/services/function-list/statistical.ts | 43 +++-- 12 files changed, 373 insertions(+), 137 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/averageifs/index.ts diff --git a/packages/engine-formula/src/engine/utils/value-object.ts b/packages/engine-formula/src/engine/utils/value-object.ts index 0610cb43fa2..f9d367f0f32 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 type { BaseValueObject } from '../value-object/base-value-object'; +import { ErrorValueObject } from '../value-object/base-value-object'; import { NumberValueObject } from '../value-object/primitive-object'; +import { ErrorType } from '../../basics/error-type'; +import { expandArrayValueObject } from './array-object'; +import { booleanObjectIntersection, valueObjectCompare } from './object-compare'; export function convertTonNumber(valueObject: BaseValueObject) { const currentValue = valueObject.getValue(); @@ -154,3 +158,55 @@ export function calculateMaxDimensions(variants: BaseValueObject[]) { 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.NA)); + } + } + + return null; +} + +export function getBooleanResults(variants: BaseValueObject[], maxRowLength: number, maxColumnLength: number) { + 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; + } + + const resultArrayObject = valueObjectCompare(range, criteriaValueObject); + + 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; +} diff --git a/packages/engine-formula/src/functions/math/sumifs/index.ts b/packages/engine-formula/src/functions/math/sumifs/index.ts index eeec1e1cf3f..e8e34ae6937 100644 --- a/packages/engine-formula/src/functions/math/sumifs/index.ts +++ b/packages/engine-formula/src/functions/math/sumifs/index.ts @@ -15,9 +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 } from '../../../engine/utils/value-object'; +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'; @@ -49,45 +47,14 @@ export class Sumifs extends BaseFunction { const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); - const sumRowLength = (sumRange as ArrayValueObject).getRowCount(); - const sumColumnLength = (sumRange as ArrayValueObject).getColumnCount(); + const errorArray = getErrorArray(variants, sumRange, maxRowLength, maxColumnLength); - 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)); - - criteriaArray.iterator((criteriaValueObject, rowIndex, columnIndex) => { - if (!criteriaValueObject) { - return; - } - - const resultArrayObject = valueObjectCompare(range, criteriaValueObject); - - if (booleanResults[rowIndex] === undefined) { - booleanResults[rowIndex] = []; - } - - if (booleanResults[rowIndex][columnIndex] === undefined) { - booleanResults[rowIndex][columnIndex] = resultArrayObject; - return; - } - - booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject); - }); + if (errorArray) { + return errorArray; } + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + return this._aggregateResults(sumRange, booleanResults); } 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..1d39644e29e --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts @@ -0,0 +1,162 @@ +/** + * 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 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.5]]); + }); + + 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.5], [3], [ErrorType.DIV_BY_ZERO]]); + }); + + 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], [ErrorType.DIV_BY_ZERO]]); + }); + + 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], [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..cb2ad77701f --- /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); + + 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/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 0bc808257ba..b6d2b425a40 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -32,11 +32,13 @@ import { Maxifs } from './maxifs'; import { Averagea } from './averagea'; import { Minifs } from './minifs'; import { Averageif } from './averageif'; +import { Averageifs } from './averageifs'; export const functionStatistical = [ [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], [Max, FUNCTION_NAMES_STATISTICAL.MAX], [Min, FUNCTION_NAMES_STATISTICAL.MIN], diff --git a/packages/engine-formula/src/functions/statistical/maxifs/index.ts b/packages/engine-formula/src/functions/statistical/maxifs/index.ts index 4d22ee8e302..09de3662611 100644 --- a/packages/engine-formula/src/functions/statistical/maxifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/maxifs/index.ts @@ -15,9 +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 } from '../../../engine/utils/value-object'; +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'; @@ -49,44 +47,14 @@ export class Maxifs extends BaseFunction { const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); - const sumRowLength = (maxRange as ArrayValueObject).getRowCount(); - const sumColumnLength = (maxRange as ArrayValueObject).getColumnCount(); + const errorArray = getErrorArray(variants, maxRange, maxRowLength, maxColumnLength); - 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 resultArrayObject = valueObjectCompare(range, criteriaValueObject); - - if (booleanResults[rowIndex] === undefined) { - booleanResults[rowIndex] = []; - } - - if (booleanResults[rowIndex][columnIndex] === undefined) { - booleanResults[rowIndex][columnIndex] = resultArrayObject; - } - - booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject); - }); + if (errorArray) { + return errorArray; } + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + return this._aggregateResults(maxRange, booleanResults); } diff --git a/packages/engine-formula/src/functions/statistical/minifs/index.ts b/packages/engine-formula/src/functions/statistical/minifs/index.ts index d8ae37bb12b..7d54be37243 100644 --- a/packages/engine-formula/src/functions/statistical/minifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/minifs/index.ts @@ -15,9 +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 } from '../../../engine/utils/value-object'; +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'; @@ -49,44 +47,14 @@ export class Minifs extends BaseFunction { const { maxRowLength, maxColumnLength } = calculateMaxDimensions(variants); - const minRowLength = (minRange as ArrayValueObject).getRowCount(); - const minColumnLength = (minRange as ArrayValueObject).getColumnCount(); + const errorArray = getErrorArray(variants, minRange, maxRowLength, maxColumnLength); - 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 !== minRowLength || rangeColumnLength !== minColumnLength) { - 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 resultArrayObject = valueObjectCompare(range, criteriaValueObject); - - if (booleanResults[rowIndex] === undefined) { - booleanResults[rowIndex] = []; - } - - if (booleanResults[rowIndex][columnIndex] === undefined) { - booleanResults[rowIndex][columnIndex] = resultArrayObject; - } - - booleanResults[rowIndex][columnIndex] = booleanObjectIntersection(booleanResults[rowIndex][columnIndex], resultArrayObject); - }); + if (errorArray) { + return errorArray; } + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + return this._aggregateResults(minRange, booleanResults); } 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 4098a82c293..4db0742223c 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 @@ -85,7 +85,7 @@ export default { }, }, 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: [ { @@ -94,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 126 range.' }, + criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 126 criteria.' }, }, }, BETA_DIST: { 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 eccc4dca340..cd413bef5b8 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 @@ -74,7 +74,7 @@ export default { }, AVERAGEIFS: { description: '複数の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', - abstract: '複数の検索条件に一致するすべてのセルの平均値 (算術平均) を返します。', + abstract: '複数の検索条件に一致するすべてのセルの平均値 (算術平均) を返します', links: [ { title: '指導', @@ -82,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: '追加エリア。 エリアは最大126エリアまで入力できます。' }, + criteria2: { name: '条件 2', detail: '追加の関連付け条件。 条件は126個まで入力できます。' }, }, }, BETA_DIST: { 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 c3334a73712..5c93654bb0c 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 @@ -85,7 +85,7 @@ export default { }, }, AVERAGEIFS: { - description: '返回满足多个条件的所有单元格的平均值(算术平均值)', + description: '返回满足多个条件的所有单元格的平均值(算术平均值)。', abstract: '返回满足多个条件的所有单元格的平均值(算术平均值)', links: [ { @@ -94,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: '附加区域。 最多可以输入 126 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 126 个条件。' }, }, }, BETA_DIST: { 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 6e144c95425..73c06cb656d 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -121,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, + }, ], }, { @@ -1365,7 +1386,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, }, @@ -1379,14 +1400,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, }, @@ -1482,21 +1503,21 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ { name: 'formula.functionList.MINIFS.functionParameter.criteria1.name', detail: 'formula.functionList.MINIFS.functionParameter.criteria1.detail', - example: '"<100"', + example: '">10"', require: 1, repeat: 0, }, { name: 'formula.functionList.MINIFS.functionParameter.criteriaRange2.name', detail: 'formula.functionList.MINIFS.functionParameter.criteriaRange2.detail', - example: 'B1:B20', + example: 'C1:C20', require: 0, repeat: 1, }, { name: 'formula.functionList.MINIFS.functionParameter.criteria2.name', detail: 'formula.functionList.MINIFS.functionParameter.criteria2.detail', - example: 'B1:B20', + example: '<20', require: 0, repeat: 1, }, From bdc62f0502fbe5b7eb11db8fc49f5f40d6308d68 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Thu, 20 Jun 2024 18:26:12 +0800 Subject: [PATCH 05/18] feat(formula): add countif --- .../utils/__tests__/object-compare.spec.ts | 13 +++- .../src/engine/utils/object-compare.ts | 14 ++++ .../countif/__tests__/index.spec.ts | 76 +++++++++++++++++++ .../functions/statistical/countif/index.ts | 64 ++++++++++++++++ .../src/functions/statistical/function-map.ts | 2 + .../locale/function-list/statistical/en-US.ts | 6 +- .../locale/function-list/statistical/ja-JP.ts | 6 +- .../locale/function-list/statistical/zh-CN.ts | 6 +- .../src/services/function-list/statistical.ts | 10 +-- 9 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/countif/index.ts 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..6de5af925c9 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 @@ -18,7 +18,7 @@ import { describe, expect, it } from 'vitest'; import { ArrayValueObject, transformToValue } from '../../value-object/array-value-object'; import { BooleanValueObject, NumberValueObject, StringValueObject } from '../../value-object/primitive-object'; -import { valueObjectCompare } from '../object-compare'; +import { isNumericComparison, valueObjectCompare } from '../object-compare'; import { compareToken } from '../../../basics/token'; const range = ArrayValueObject.create(/*ts*/ `{ @@ -208,5 +208,16 @@ describe('Test object compare', () => { expect(value.getValue()).toStrictEqual(result[i]); }); }); + it('Function isNumericComparison', () => { + expect(isNumericComparison('>40')).toBe(true); + expect(isNumericComparison('<=100')).toBe(true); + expect(isNumericComparison('=5')).toBe(true); + expect(isNumericComparison('test*')).toBe(false); + expect(isNumericComparison('=test')).toBe(false); + expect(isNumericComparison('> 40')).toBe(true); + expect(isNumericComparison('>=3.14')).toBe(true); + expect(isNumericComparison(5)).toBe(true); + expect(isNumericComparison(true)).toBe(false); + }); }); }); diff --git a/packages/engine-formula/src/engine/utils/object-compare.ts b/packages/engine-formula/src/engine/utils/object-compare.ts index 47f013c4ee8..8222a009341 100644 --- a/packages/engine-formula/src/engine/utils/object-compare.ts +++ b/packages/engine-formula/src/engine/utils/object-compare.ts @@ -94,3 +94,17 @@ export function booleanObjectIntersection(valueObject1: BaseValueObject, valueOb return BooleanValueObject.create(false); }); } + +export function isNumericComparison(condition: string | number | boolean): boolean { + if (typeof condition === 'number') { + return true; + } else if (typeof condition === 'boolean') { + return false; + } + + // Combined regular expression for numeric comparisons + const numericComparisonPattern = /^[<>]?=?\s*\d+(\.\d+)?$/; + + // Test the condition against the pattern + return numericComparisonPattern.test(condition.trim()); +} 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..ed960c1b4ff --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts @@ -0,0 +1,76 @@ +/** + * 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 } 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'; + +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('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]]); + }); + }); +}); 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..0fefcf09c18 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countif/index.ts @@ -0,0 +1,64 @@ +/** + * 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 { isNumericComparison, valueObjectCompare } from '../../../engine/utils/object-compare'; +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 Countif extends BaseFunction { + override minParams = 2; + + override maxParams = 3; + + 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.map((criteriaItem) => this._handleSingleObject(range, criteriaItem)); + } + + return this._handleSingleObject(range, criteria); + } + + private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { + const resultArrayObject = valueObjectCompare(range, criteria); + + // averageRange has the same dimensions as range + const averageRangeArray = averageRange + ? (averageRange as ArrayValueObject).slice( + [0, (range as ArrayValueObject).getRowCount()], + [0, (range as ArrayValueObject).getColumnCount()] + ) + : (range as ArrayValueObject); + + if (!averageRangeArray) { + return ErrorValueObject.create(ErrorType.VALUE); + } + + const picked = averageRangeArray.pick(resultArrayObject as ArrayValueObject); + // If the condition is a numeric comparison, only numbers are counted, otherwise text is counted. + const isNumeric = isNumericComparison(criteria.getValue()); + return isNumeric ? picked.count() : picked.countA(); + } +} diff --git a/packages/engine-formula/src/functions/statistical/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index b6d2b425a40..7287f2e0a7b 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -33,6 +33,7 @@ import { Averagea } from './averagea'; import { Minifs } from './minifs'; import { Averageif } from './averageif'; import { Averageifs } from './averageifs'; +import { Countif } from './countif'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], @@ -40,6 +41,7 @@ export const functionStatistical = [ [Averageif, FUNCTION_NAMES_STATISTICAL.AVERAGEIF], [Averageifs, FUNCTION_NAMES_STATISTICAL.AVERAGEIFS], [Count, FUNCTION_NAMES_STATISTICAL.COUNT], + [Countif, FUNCTION_NAMES_STATISTICAL.COUNTIF], [Max, FUNCTION_NAMES_STATISTICAL.MAX], [Min, FUNCTION_NAMES_STATISTICAL.MIN], [Min, FUNCTION_NAMES_STATISTICAL.MIN], 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 4db0742223c..de2ef8a1267 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 @@ -339,7 +339,7 @@ export default { }, }, 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: [ { @@ -348,8 +348,8 @@ 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: { 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 cd413bef5b8..ecc88364d30 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 @@ -315,7 +315,7 @@ export default { }, COUNTIF: { description: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します。', - abstract: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します。', + abstract: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します', links: [ { title: '指導', @@ -323,8 +323,8 @@ 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: { 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 5c93654bb0c..6ffc401ab13 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 @@ -339,7 +339,7 @@ export default { }, }, COUNTIF: { - description: '计算区域内符合给定条件的单元格的数量', + description: '计算区域内符合给定条件的单元格的数量。', abstract: '计算区域内符合给定条件的单元格的数量', links: [ { @@ -348,8 +348,8 @@ 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: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index 73c06cb656d..6bbf3c53c97 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -518,16 +518,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, }, From c134e1dcf44c128d104ac7faf10d3c451e75aebf Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Thu, 20 Jun 2024 21:20:43 +0800 Subject: [PATCH 06/18] feat(formula): add countifs --- .../countifs/__tests__/index.spec.ts | 201 ++++++++++++++++++ .../functions/statistical/countifs/index.ts | 83 ++++++++ .../src/functions/statistical/function-map.ts | 2 + .../locale/function-list/statistical/en-US.ts | 16 +- .../locale/function-list/statistical/ja-JP.ts | 16 +- .../locale/function-list/statistical/zh-CN.ts | 20 +- .../src/services/function-list/statistical.ts | 24 ++- 7 files changed, 334 insertions(+), 28 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/countifs/index.ts 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..5047b0c8bfb --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts @@ -0,0 +1,201 @@ +/** + * 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('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]]); + }); + }); +}); 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..c0cabb16ab5 --- /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); + + 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 7287f2e0a7b..22089d45a6b 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -34,6 +34,7 @@ import { Minifs } from './minifs'; import { Averageif } from './averageif'; import { Averageifs } from './averageifs'; import { Countif } from './countif'; +import { Countifs } from './countifs'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], @@ -42,6 +43,7 @@ export const functionStatistical = [ [Averageifs, FUNCTION_NAMES_STATISTICAL.AVERAGEIFS], [Count, FUNCTION_NAMES_STATISTICAL.COUNT], [Countif, FUNCTION_NAMES_STATISTICAL.COUNTIF], + [Countifs, FUNCTION_NAMES_STATISTICAL.COUNTIFS], [Max, FUNCTION_NAMES_STATISTICAL.MAX], [Min, FUNCTION_NAMES_STATISTICAL.MIN], [Min, FUNCTION_NAMES_STATISTICAL.MIN], 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 de2ef8a1267..b643d81f8e9 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 @@ -97,8 +97,8 @@ export default { 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 126 range.' }, - criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 126 criteria.' }, + 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: { @@ -353,7 +353,7 @@ export default { }, }, 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: [ { @@ -362,8 +362,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: { @@ -968,8 +970,8 @@ export default { 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 126 range.' }, - criteria2: { name: 'criteria2', detail: 'Additional associated criteria. You can enter up to 126 criteria.' }, + 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 ecc88364d30..df2865b8b9d 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 @@ -85,8 +85,8 @@ export default { averageRange: { name: '平均範囲', detail: '平均する 1 つまたは複数のセル (数値、または数値を含む名前、配列、セル参照) を指定します。' }, criteriaRange1: { name: '条件範囲 1', detail: '条件で評価するセルのセットです。' }, criteria1: { name: '条件 1', detail: '平均を計算するセルを定義するために使用されます。 たとえば、条件は 32、"32"、">32"、"apple"、または B4 のように表現できます。' }, - criteriaRange2: { name: '条件範囲 2', detail: '追加エリア。 エリアは最大126エリアまで入力できます。' }, - criteria2: { name: '条件 2', detail: '追加の関連付け条件。 条件は126個まで入力できます。' }, + criteriaRange2: { name: '条件範囲 2', detail: '追加の範囲。 最大 127 の範囲のペアを入力できます。' }, + criteria2: { name: '条件 2', detail: '追加対応する条件です。 最大 127 条件のペアを入力できます。' }, }, }, BETA_DIST: { @@ -329,7 +329,7 @@ export default { }, COUNTIFS: { description: '指定された範囲に含まれるセルのうち、複数の検索条件に一致するセルの個数を返します。', - abstract: '指定された範囲に含まれるセルのうち、複数の検索条件に一致するセルの個数を返します。', + abstract: '指定された範囲に含まれるセルのうち、複数の検索条件に一致するセルの個数を返します', links: [ { title: '指導', @@ -337,8 +337,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: { @@ -931,8 +933,8 @@ export default { minRange: { name: '最少範囲', detail: '最小値を求めるセルの実際の範囲です。' }, criteriaRange1: { name: '条件範囲 1', detail: '条件で評価するセルのセットです。' }, criteria1: { name: '条件 1', detail: '最小として評価されるセルを定義する、数値、式、またはテキストの形式での条件です。 同じ条件セットを、MAXIFS、SUMIFS、および AVERAGEIFS 関数に対して使用できます。' }, - criteriaRange2: { name: '条件範囲 2', detail: '追加の範囲と対応する条件です。 最大 126 個の範囲/条件ペアを入力できます。' }, - criteria2: { name: '条件 2', detail: '追加の範囲と対応する条件です。 最大 126 個の範囲/条件ペアを入力できます。' }, + 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 6ffc401ab13..0df305c7f59 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 @@ -97,8 +97,8 @@ export default { averageRange: { name: '平均值范围', detail: '要计算平均值的一个或多个单元格,其中包含数字或包含数字的名称、数组或引用。' }, criteriaRange1: { name: '条件范围 1', detail: '是一组用于条件计算的单元格。' }, criteria1: { name: '条件 1', detail: '用来定义将计算平均值的单元格。 例如,条件可以表示为 32、"32"、">32"、"苹果" 或 B4' }, - criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 126 个区域。' }, - criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 126 个条件。' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, BETA_DIST: { @@ -353,7 +353,7 @@ export default { }, }, COUNTIFS: { - description: '计算区域内符合多个条件的单元格的数量', + description: '计算区域内符合多个条件的单元格的数量。', abstract: '计算区域内符合多个条件的单元格的数量', links: [ { @@ -362,8 +362,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: { @@ -903,8 +905,8 @@ export default { maxRange: { name: '最大值范围', detail: '确定最大值的实际单元格区域。' }, criteriaRange1: { name: '条件范围 1', detail: '是一组用于条件计算的单元格。' }, criteria1: { name: '条件 1', detail: '用于确定哪些单元格是最大值的条件,格式为数字、表达式或文本。 一组相同的条件适用于 MINIFS、SUMIFS 和 AVERAGEIFS 函数。' }, - criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 126 个区域。' }, - criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 126 个条件。' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, MEDIAN: { @@ -968,8 +970,8 @@ export default { minRange: { name: '最小值范围', detail: '确定最小值的实际单元格区域。' }, criteriaRange1: { name: '条件范围 1', detail: '是一组用于条件计算的单元格。' }, criteria1: { name: '条件 1', detail: '用于确定哪些单元格是最小值的条件,格式为数字、表达式或文本。 一组相同的条件适用于 MAXIFS、SUMIFS 和 AVERAGEIFS 函数。' }, - criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 126 个区域。' }, - criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 126 个条件。' }, + criteriaRange2: { name: '条件范围 2', detail: '附加区域。 最多可以输入 127 个区域。' }, + criteria2: { name: '条件 2', detail: '附加关联条件。 最多可以输入 127 个条件。' }, }, }, MODE_MULT: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index 6bbf3c53c97..f5e7e85a348 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -540,19 +540,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, + }, ], }, { From 509a627110d4ecb8da12430704fbc11ce870989a Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Fri, 21 Jun 2024 19:18:27 +0800 Subject: [PATCH 07/18] feat(formula): add countifs --- .../src/engine/utils/value-object.ts | 27 ++++++++--- .../engine/value-object/primitive-object.ts | 32 ++++++++++++- .../meta/compare/__tests__/index.spec.ts | 45 +++++++++++++++++++ .../functions/statistical/countif/index.ts | 18 ++------ .../functions/statistical/countifs/index.ts | 2 +- 5 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts diff --git a/packages/engine-formula/src/engine/utils/value-object.ts b/packages/engine-formula/src/engine/utils/value-object.ts index f9d367f0f32..6bb4899f047 100644 --- a/packages/engine-formula/src/engine/utils/value-object.ts +++ b/packages/engine-formula/src/engine/utils/value-object.ts @@ -20,10 +20,10 @@ import type { BaseReferenceObject, FunctionVariantType } from '../reference-obje import type { ArrayValueObject } from '../value-object/array-value-object'; import type { BaseValueObject } from '../value-object/base-value-object'; import { ErrorValueObject } from '../value-object/base-value-object'; -import { NumberValueObject } from '../value-object/primitive-object'; +import { BooleanValueObject, NumberValueObject } from '../value-object/primitive-object'; import { ErrorType } from '../../basics/error-type'; import { expandArrayValueObject } from './array-object'; -import { booleanObjectIntersection, valueObjectCompare } from './object-compare'; +import { booleanObjectIntersection, isNumericComparison, valueObjectCompare } from './object-compare'; export function convertTonNumber(valueObject: BaseValueObject) { const currentValue = valueObject.getValue(); @@ -171,14 +171,14 @@ export function getErrorArray(variants: BaseValueObject[], sumRange: BaseValueOb 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)); + return expandArrayValueObject(maxRowLength, maxColumnLength, ErrorValueObject.create(ErrorType.VALUE)); } } return null; } -export function getBooleanResults(variants: BaseValueObject[], maxRowLength: number, maxColumnLength: number) { +export function getBooleanResults(variants: BaseValueObject[], maxRowLength: number, maxColumnLength: number, isNumberSensitive: boolean = false) { const booleanResults: BaseValueObject[][] = []; for (let i = 0; i < variants.length; i++) { @@ -193,7 +193,14 @@ export function getBooleanResults(variants: BaseValueObject[], maxRowLength: num return; } - const resultArrayObject = valueObjectCompare(range, criteriaValueObject); + // range must be an ArrayValueObject, criteria must be a BaseValueObject + let resultArrayObject = valueObjectCompare(range, criteriaValueObject); + + // When comparing non-numbers and numbers, countifs does not take the result + const isNumeric = isNumericComparison(criteriaValueObject.getValue()); + if (isNumberSensitive && isNumeric) { + resultArrayObject = removeNonNumberValueObject(resultArrayObject as ArrayValueObject, range as ArrayValueObject); + } if (booleanResults[rowIndex] === undefined) { booleanResults[rowIndex] = []; @@ -210,3 +217,13 @@ export function getBooleanResults(variants: BaseValueObject[], maxRowLength: num return booleanResults; } + +function removeNonNumberValueObject(array: ArrayValueObject, range: ArrayValueObject) { + return array.mapValue((valueObject, r, c) => { + if (range.get(r, c)?.isNumber()) { + return valueObject; + } else { + return BooleanValueObject.create(false); + } + }); +} 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..2c988ab1d4f 100644 --- a/packages/engine-formula/src/engine/value-object/primitive-object.ts +++ b/packages/engine-formula/src/engine/value-object/primitive-object.ts @@ -270,7 +270,37 @@ export class BooleanValueObject extends BaseValueObject { } override compare(valueObject: BaseValueObject, operator: compareToken): BaseValueObject { - return this._convertTonNumber().compare(valueObject, operator); + if (valueObject.isArray()) { + return valueObject.compare(this, reverseCompareOperator(operator)); + } + + return this.compareBy(valueObject.getValue(), operator); + } + + override compareBy(value: string | number | boolean, operator: compareToken): BaseValueObject { + let result = false; + // FALSE > 0 and FALSE > "Univer" get TRUE + if (typeof value === 'string' || typeof value === 'number') { + result = this._compareString(operator); + } else if (typeof value === 'boolean') { + const booleanNumber = NumberValueObject.create(value ? 1 : 0); + return this._convertTonNumber().compare(booleanNumber, operator); + } + + return BooleanValueObject.create(result); + } + + private _compareString(operator: compareToken): boolean { + switch (operator) { + case compareToken.EQUALS: + case compareToken.GREATER_THAN: + case compareToken.GREATER_THAN_OR_EQUAL: + return true; + case compareToken.LESS_THAN: + case compareToken.LESS_THAN_OR_EQUAL: + case compareToken.NOT_EQUAL: + return false; + } } override concatenateFront(valueObject: BaseValueObject): BaseValueObject { 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..9aeb5828ca0 --- /dev/null +++ b/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts @@ -0,0 +1,45 @@ +/** + * 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_META } from '../../function-names'; +import { Compare } from '../index'; +import { BooleanValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { compareToken } from '../../../../basics/token'; + +describe('Test compare function', () => { + const testFunction = new Compare(FUNCTION_NAMES_META.COMPARE); + + describe('Compoare', () => { + 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); + }); + }); +}); diff --git a/packages/engine-formula/src/functions/statistical/countif/index.ts b/packages/engine-formula/src/functions/statistical/countif/index.ts index 0fefcf09c18..7b59e4da3c2 100644 --- a/packages/engine-formula/src/functions/statistical/countif/index.ts +++ b/packages/engine-formula/src/functions/statistical/countif/index.ts @@ -23,7 +23,7 @@ import { BaseFunction } from '../../base-function'; export class Countif extends BaseFunction { override minParams = 2; - override maxParams = 3; + override maxParams = 2; override calculate(range: BaseValueObject, criteria: BaseValueObject) { if (range.isError() || criteria.isError()) { @@ -41,22 +41,10 @@ export class Countif extends BaseFunction { return this._handleSingleObject(range, criteria); } - private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { + private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject) { const resultArrayObject = valueObjectCompare(range, criteria); - // averageRange has the same dimensions as range - const averageRangeArray = averageRange - ? (averageRange as ArrayValueObject).slice( - [0, (range as ArrayValueObject).getRowCount()], - [0, (range as ArrayValueObject).getColumnCount()] - ) - : (range as ArrayValueObject); - - if (!averageRangeArray) { - return ErrorValueObject.create(ErrorType.VALUE); - } - - const picked = averageRangeArray.pick(resultArrayObject as ArrayValueObject); + const picked = (range as ArrayValueObject).pick(resultArrayObject as ArrayValueObject); // If the condition is a numeric comparison, only numbers are counted, otherwise text is counted. const isNumeric = isNumericComparison(criteria.getValue()); return isNumeric ? picked.count() : picked.countA(); diff --git a/packages/engine-formula/src/functions/statistical/countifs/index.ts b/packages/engine-formula/src/functions/statistical/countifs/index.ts index c0cabb16ab5..7cd421685d3 100644 --- a/packages/engine-formula/src/functions/statistical/countifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/countifs/index.ts @@ -46,7 +46,7 @@ export class Countifs extends BaseFunction { return errorArray; } - const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); return this._aggregateResults(booleanResults); } From e615c74f2e3f202c6847bf697fcfd7a3c56616bf Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Sat, 22 Jun 2024 16:00:03 +0800 Subject: [PATCH 08/18] feat(formula): add countblank function --- .../__tests__/array-value-object.spec.ts | 8 +- .../engine/value-object/array-value-object.ts | 6 +- .../countblank/__tests__/index.spec.ts | 78 +++++++++++++++++++ .../functions/statistical/countblank/index.ts | 38 +++++++++ .../src/functions/statistical/function-map.ts | 9 ++- .../locale/function-list/statistical/en-US.ts | 5 +- .../locale/function-list/statistical/ja-JP.ts | 5 +- .../locale/function-list/statistical/zh-CN.ts | 5 +- .../src/services/function-list/statistical.ts | 11 +-- 9 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/countblank/index.ts 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..e254b3192c1 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; 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..5ec856a2762 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts @@ -0,0 +1,78 @@ +/** + * 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.NA); + const result = testFunction.calculate(range); + expect(result.getValue()).toBe(0); + }); + 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); + }); + }); +}); 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..f6bc70ac6ea --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/countblank/index.ts @@ -0,0 +1,38 @@ +/** + * 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.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/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 22089d45a6b..9b0ba02791d 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -35,6 +35,7 @@ import { Averageif } from './averageif'; import { Averageifs } from './averageifs'; import { Countif } from './countif'; import { Countifs } from './countifs'; +import { Countblank } from './countblank'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], @@ -42,12 +43,14 @@ export const functionStatistical = [ [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], + [Maxifs, FUNCTION_NAMES_STATISTICAL.MAXIFS], [Min, FUNCTION_NAMES_STATISTICAL.MIN], - [Min, FUNCTION_NAMES_STATISTICAL.MIN], - [Counta, FUNCTION_NAMES_STATISTICAL.COUNTA], + [Minifs, FUNCTION_NAMES_STATISTICAL.MINIFS], [StdevP, FUNCTION_NAMES_STATISTICAL.STDEV_P], [StdevS, FUNCTION_NAMES_STATISTICAL.STDEV_S], [Stdeva, FUNCTION_NAMES_STATISTICAL.STDEVA], @@ -56,6 +59,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], - [Minifs, FUNCTION_NAMES_STATISTICAL.MINIFS], ]; 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 b643d81f8e9..8787a0687c1 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 @@ -325,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: [ { @@ -334,8 +334,7 @@ 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: { 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 df2865b8b9d..d220c8d6239 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 @@ -301,7 +301,7 @@ export default { }, COUNTBLANK: { description: '指定された範囲に含まれる空白セルの個数を返します。', - abstract: '指定された範囲に含まれる空白セルの個数を返します。', + abstract: '指定された範囲に含まれる空白セルの個数を返します', links: [ { title: '指導', @@ -309,8 +309,7 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '範囲', detail: '空白セルの個数を求めるセル範囲を指定します。' }, }, }, COUNTIF: { 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 0df305c7f59..33d30f49d94 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 @@ -325,7 +325,7 @@ export default { }, }, COUNTBLANK: { - description: '计算区域内空白单元格的数量', + description: '计算区域内空白单元格的数量。', abstract: '计算区域内空白单元格的数量', links: [ { @@ -334,8 +334,7 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + range: { name: '范围', detail: '需要计算其中空白单元格个数的区域。' }, }, }, COUNTIF: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index f5e7e85a348..547af25bce1 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -496,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, From 7d57500224e6f9e3409939b70137a773fb251575 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Sat, 22 Jun 2024 17:11:37 +0800 Subject: [PATCH 09/18] feat(formula): add mina function --- .../src/functions/statistical/function-map.ts | 2 + .../src/functions/statistical/min/index.ts | 8 +- .../statistical/mina/__tests__/index.spec.ts | 182 ++++++++++++++++++ .../src/functions/statistical/mina/index.ts | 85 ++++++++ .../locale/function-list/statistical/en-US.ts | 4 +- .../locale/function-list/statistical/ja-JP.ts | 8 +- .../locale/function-list/statistical/zh-CN.ts | 6 +- .../src/services/function-list/statistical.ts | 16 +- 8 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/mina/index.ts diff --git a/packages/engine-formula/src/functions/statistical/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 9b0ba02791d..94ff3af89b1 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -36,6 +36,7 @@ import { Averageifs } from './averageifs'; import { Countif } from './countif'; import { Countifs } from './countifs'; import { Countblank } from './countblank'; +import { Mina } from './mina'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], @@ -50,6 +51,7 @@ export const functionStatistical = [ [Max, FUNCTION_NAMES_STATISTICAL.MAX], [Maxifs, FUNCTION_NAMES_STATISTICAL.MAXIFS], [Min, FUNCTION_NAMES_STATISTICAL.MIN], + [Mina, FUNCTION_NAMES_STATISTICAL.MINA], [Minifs, FUNCTION_NAMES_STATISTICAL.MINIFS], [StdevP, FUNCTION_NAMES_STATISTICAL.STDEV_P], [StdevS, FUNCTION_NAMES_STATISTICAL.STDEV_S], 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..0f90cc5852d --- /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 min function', () => { + const testFunction = new Mina(FUNCTION_NAMES_STATISTICAL.MIN); + + describe('Min', () => { + 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/sheets-formula/src/locale/function-list/statistical/en-US.ts b/packages/sheets-formula/src/locale/function-list/statistical/en-US.ts index 8787a0687c1..e6280d9a7c4 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 @@ -952,8 +952,8 @@ 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: { 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 d220c8d6239..1299beafb48 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 @@ -901,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: { @@ -915,8 +915,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '値 1', detail: '最小値を計算する最初の数値、セル参照、またはセル範囲。' }, + value2: { name: '値 2', detail: '最小値を計算するために、最大 255 個の追加の数値、セル参照、またはセル範囲を含めることができます。' }, }, }, MINIFS: { 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 33d30f49d94..4c595266440 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 @@ -943,7 +943,7 @@ export default { }, }, MINA: { - description: '返回参数列表中的最小值,包括数字、文本和逻辑值', + description: '返回参数列表中的最小值,包括数字、文本和逻辑值。', abstract: '返回参数列表中的最小值,包括数字、文本和逻辑值', links: [ { @@ -952,8 +952,8 @@ export default { }, ], functionParameter: { - number1: { name: 'number1', detail: 'first' }, - number2: { name: 'number2', detail: 'second' }, + value1: { name: '值 1', detail: '要计算最小值的第一个数字、单元格引用或单元格区域。' }, + value2: { name: '值 2', detail: '要计算最小值的其他数字、单元格引用或单元格区域,最多可包含 255 个。' }, }, }, MINIFS: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index 547af25bce1..9b9a0160152 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -1472,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, }, ], }, @@ -1524,7 +1524,7 @@ export const FUNCTION_LIST_STATISTICAL: IFunctionInfo[] = [ { name: 'formula.functionList.MINIFS.functionParameter.criteria2.name', detail: 'formula.functionList.MINIFS.functionParameter.criteria2.detail', - example: '<20', + example: '"<20"', require: 0, repeat: 1, }, From 20b0914845470294e7a0f8d925e2e7d2aa9edf80 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Sat, 22 Jun 2024 17:34:27 +0800 Subject: [PATCH 10/18] feat(formula): add maxa function --- .../src/functions/statistical/function-map.ts | 2 + .../statistical/maxa/__tests__/index.spec.ts | 182 ++++++++++++++++++ .../src/functions/statistical/maxa/index.ts | 85 ++++++++ .../statistical/mina/__tests__/index.spec.ts | 6 +- .../locale/function-list/statistical/en-US.ts | 6 +- .../locale/function-list/statistical/ja-JP.ts | 12 +- .../locale/function-list/statistical/zh-CN.ts | 6 +- .../src/services/function-list/statistical.ts | 14 +- 8 files changed, 291 insertions(+), 22 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/maxa/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/maxa/index.ts diff --git a/packages/engine-formula/src/functions/statistical/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 94ff3af89b1..8ee4c3d42b9 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -37,6 +37,7 @@ import { Countif } from './countif'; import { Countifs } from './countifs'; import { Countblank } from './countblank'; import { Mina } from './mina'; +import { Maxa } from './maxa'; export const functionStatistical = [ [Average, FUNCTION_NAMES_STATISTICAL.AVERAGE], @@ -49,6 +50,7 @@ export const functionStatistical = [ [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], [Mina, FUNCTION_NAMES_STATISTICAL.MINA], 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/mina/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts index 0f90cc5852d..54a5f57cc72 100644 --- a/packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/mina/__tests__/index.spec.ts @@ -23,10 +23,10 @@ import { ArrayValueObject, transformToValueObject } from '../../../../engine/val import { ErrorType } from '../../../../basics/error-type'; import { ErrorValueObject } from '../../../../engine/value-object/base-value-object'; -describe('Test min function', () => { - const testFunction = new Mina(FUNCTION_NAMES_STATISTICAL.MIN); +describe('Test mina function', () => { + const testFunction = new Mina(FUNCTION_NAMES_STATISTICAL.MINA); - describe('Min', () => { + describe('Mina', () => { it('Var1 is number, var2 is number', () => { const var1 = NumberValueObject.create(1); const var2 = NumberValueObject.create(2); 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 e6280d9a7c4..5977576cc98 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 @@ -878,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: [ { @@ -887,8 +887,8 @@ 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: { 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 1299beafb48..b1c28c5e1ba 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 @@ -834,7 +834,7 @@ export default { }, MAX: { description: '引数リストに含まれる最大の数値を返します。', - abstract: '引数リストに含まれる最大の数値を返します。', + abstract: '引数リストに含まれる最大の数値を返します', links: [ { title: '指導', @@ -842,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: '指導', @@ -856,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: { 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 4c595266440..86cce0b07a6 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 @@ -878,7 +878,7 @@ export default { }, }, MAXA: { - description: '返回参数列表中的最大值,包括数字、文本和逻辑值', + description: '返回参数列表中的最大值,包括数字、文本和逻辑值。', abstract: '返回参数列表中的最大值,包括数字、文本和逻辑值', links: [ { @@ -887,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: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index 9b9a0160152..ad4470fcb57 100644 --- a/packages/sheets-formula/src/services/function-list/statistical.ts +++ b/packages/sheets-formula/src/services/function-list/statistical.ts @@ -1362,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, }, ], }, From 52bba034561f402be5671822e921f69a9ce91d8c Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Sat, 22 Jun 2024 20:11:07 +0800 Subject: [PATCH 11/18] feat(formula): add avedev function --- .../avedev/__tests__/index.spec.ts | 143 ++++++++++++++++++ .../src/functions/statistical/avedev/index.ts | 130 ++++++++++++++++ .../average/__tests__/index.spec.ts | 36 +++++ .../src/functions/statistical/function-map.ts | 2 + .../locale/function-list/statistical/en-US.ts | 6 +- .../locale/function-list/statistical/ja-JP.ts | 12 +- .../locale/function-list/statistical/zh-CN.ts | 6 +- .../src/services/function-list/statistical.ts | 6 +- 8 files changed, 326 insertions(+), 15 deletions(-) create mode 100644 packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts create mode 100644 packages/engine-formula/src/functions/statistical/avedev/index.ts 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..4045f581511 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts @@ -0,0 +1,143 @@ +/** + * 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 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..bccaf2c5a87 --- /dev/null +++ b/packages/engine-formula/src/functions/statistical/avedev/index.ts @@ -0,0 +1,130 @@ +/** + * 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, 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'; + +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)); + } + } + + 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/function-map.ts b/packages/engine-formula/src/functions/statistical/function-map.ts index 8ee4c3d42b9..59142cf7e7c 100644 --- a/packages/engine-formula/src/functions/statistical/function-map.ts +++ b/packages/engine-formula/src/functions/statistical/function-map.ts @@ -38,8 +38,10 @@ 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], 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 5977576cc98..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: { 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 b1c28c5e1ba..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: { 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 86cce0b07a6..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: { diff --git a/packages/sheets-formula/src/services/function-list/statistical.ts b/packages/sheets-formula/src/services/function-list/statistical.ts index ad4470fcb57..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, }, ], }, From 43a683f17be0d7fcd39145825f3a05e0e13571e2 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Sat, 22 Jun 2024 20:48:11 +0800 Subject: [PATCH 12/18] fix(formula): fix acot negative number calculation --- .../src/functions/math/acot/__tests__/index.spec.ts | 2 +- packages/engine-formula/src/functions/math/acot/index.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) 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); From 346e22b2575178952d103759dda0173c6492034e Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Mon, 24 Jun 2024 11:22:13 +0800 Subject: [PATCH 13/18] fix(formula): sumif reports ref error --- .../functions/math/sumif/__tests__/index.spec.ts | 15 +++++++++++++++ .../src/functions/math/sumif/index.ts | 12 ++++++++++-- .../math/sumifs/__tests__/index.spec.ts | 16 ++++++++++++++++ .../src/functions/math/sumifs/index.ts | 2 +- 4 files changed, 42 insertions(+), 3 deletions(-) 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..b5a5a5ebfd9 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); @@ -78,5 +80,18 @@ describe('Test sumif function', () => { const resultObject = testFunction.calculate(range, criteria); expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[4], [4], [44], [444]]); }); + it('Includes REF error', async () => { + const range = ErrorValueObject.create(ErrorType.REF); + + const criteria = ArrayValueObject.create(/*ts*/ `{ + 4; + 4; + 44; + 444 + }`); + + const resultObject = testFunction.calculate(range, criteria); + expect(resultObject.getValue()).toStrictEqual(ErrorType.REF); + }); }); }); diff --git a/packages/engine-formula/src/functions/math/sumif/index.ts b/packages/engine-formula/src/functions/math/sumif/index.ts index b4e79fa7c60..e43e2da0db1 100644 --- a/packages/engine-formula/src/functions/math/sumif/index.ts +++ b/packages/engine-formula/src/functions/math/sumif/index.ts @@ -26,8 +26,16 @@ export class Sumif extends BaseFunction { override maxParams = 3; override calculate(range: BaseValueObject, criteria: BaseValueObject, sumRange?: BaseValueObject) { - if (range.isError() || criteria.isError() || sumRange?.isError()) { - return ErrorValueObject.create(ErrorType.NA); + if (range.isError()) { + return range; + } + + if (criteria.isError()) { + return criteria; + } + + if (sumRange?.isError()) { + return sumRange; } if (!range.isArray() || (sumRange && !sumRange.isArray())) { 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..42061bd6347 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); @@ -158,5 +160,19 @@ describe('Test sumifs function', () => { const resultObject = testFunction.calculate(sumRange, range1, criteria1, range2, criteria2); expect(transformToValue(resultObject.getArrayValue())).toStrictEqual([[1], [0], [0], [0]]); }); + + it('Includes REF error', async () => { + const range = ErrorValueObject.create(ErrorType.REF); + + const criteria = ArrayValueObject.create(/*ts*/ `{ + 4; + 4; + 44; + 444 + }`); + + const resultObject = testFunction.calculate(range, criteria); + expect(resultObject.getValue()).toStrictEqual(ErrorType.REF); + }); }); }); diff --git a/packages/engine-formula/src/functions/math/sumifs/index.ts b/packages/engine-formula/src/functions/math/sumifs/index.ts index e8e34ae6937..db03f3e6816 100644 --- a/packages/engine-formula/src/functions/math/sumifs/index.ts +++ b/packages/engine-formula/src/functions/math/sumifs/index.ts @@ -28,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()) { From 939516ef47fda5d7c9f0924d2a98ae539befe7d6 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Wed, 26 Jun 2024 21:11:11 +0800 Subject: [PATCH 14/18] fix(formula): minifs compare string --- .../math/sumifs/__tests__/index.spec.ts | 17 +++++++++ .../src/functions/math/sumifs/index.ts | 2 +- .../averageifs/__tests__/index.spec.ts | 37 ++++++++++++++----- .../functions/statistical/averageifs/index.ts | 2 +- .../countifs/__tests__/index.spec.ts | 12 ++++++ .../maxifs/__tests__/index.spec.ts | 17 +++++++++ .../src/functions/statistical/maxifs/index.ts | 2 +- .../minifs/__tests__/index.spec.ts | 17 +++++++++ .../src/functions/statistical/minifs/index.ts | 2 +- 9 files changed, 94 insertions(+), 14 deletions(-) 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 42061bd6347..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 @@ -45,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; diff --git a/packages/engine-formula/src/functions/math/sumifs/index.ts b/packages/engine-formula/src/functions/math/sumifs/index.ts index db03f3e6816..02801a9af3e 100644 --- a/packages/engine-formula/src/functions/math/sumifs/index.ts +++ b/packages/engine-formula/src/functions/math/sumifs/index.ts @@ -53,7 +53,7 @@ export class Sumifs extends BaseFunction { return errorArray; } - const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); return this._aggregateResults(sumRange, booleanResults); } 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 index 1d39644e29e..f571aaae13f 100644 --- a/packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/averageifs/__tests__/index.spec.ts @@ -26,7 +26,7 @@ describe('Test averageifs function', () => { describe('Averageifs', () => { it('Range and criteria', async () => { - const minRange = ArrayValueObject.create(`{ + const averageRange = ArrayValueObject.create(`{ 1; 2; 3 @@ -38,12 +38,29 @@ describe('Test averageifs function', () => { }`); const criteria = StringValueObject.create('>2'); - const resultObject = testFunction.calculate(minRange, range, criteria); + 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 minRange = ArrayValueObject.create(`{ + const averageRange = ArrayValueObject.create(`{ 1; 2; 3 @@ -61,12 +78,12 @@ describe('Test averageifs function', () => { >4 }`); - const resultObject = testFunction.calculate(minRange, range, criteria); + 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 minRange = ArrayValueObject.create(`{ + const averageRange = ArrayValueObject.create(`{ 1; 2; 3 @@ -88,12 +105,12 @@ describe('Test averageifs function', () => { const criteria2 = StringValueObject.create('<5'); - const resultObject = testFunction.calculate(minRange, range1, criteria1, range2, criteria2); + 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 minRange = ArrayValueObject.create(`{ + const averageRange = ArrayValueObject.create(`{ 1; 2; 3 @@ -119,12 +136,12 @@ describe('Test averageifs function', () => { const criteria2 = NumberValueObject.create(5); - const resultObject = testFunction.calculate(minRange, range1, criteria1, range2, criteria2); + 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 minRange = ArrayValueObject.create(`{ + const averageRange = ArrayValueObject.create(`{ 1; 2; 3 @@ -155,7 +172,7 @@ describe('Test averageifs function', () => { 4 }`); - const resultObject = testFunction.calculate(minRange, range1, criteria1, range2, criteria2); + 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 index cb2ad77701f..0b52f77a738 100644 --- a/packages/engine-formula/src/functions/statistical/averageifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/averageifs/index.ts @@ -53,7 +53,7 @@ export class Averageifs extends BaseFunction { return errorArray; } - const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); return this._aggregateResults(averageRange, booleanResults); } 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 index 5047b0c8bfb..e43921b2d81 100644 --- a/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts @@ -45,6 +45,18 @@ describe('Test countifs function', () => { 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; 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 09de3662611..25f47200d38 100644 --- a/packages/engine-formula/src/functions/statistical/maxifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/maxifs/index.ts @@ -53,7 +53,7 @@ export class Maxifs extends BaseFunction { return errorArray; } - const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); return this._aggregateResults(maxRange, booleanResults); } 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 index 905f591ff0d..9eb69f466ad 100644 --- a/packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/minifs/__tests__/index.spec.ts @@ -41,6 +41,23 @@ describe('Test minifs function', () => { 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; diff --git a/packages/engine-formula/src/functions/statistical/minifs/index.ts b/packages/engine-formula/src/functions/statistical/minifs/index.ts index 7d54be37243..876b0750ee5 100644 --- a/packages/engine-formula/src/functions/statistical/minifs/index.ts +++ b/packages/engine-formula/src/functions/statistical/minifs/index.ts @@ -53,7 +53,7 @@ export class Minifs extends BaseFunction { return errorArray; } - const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength); + const booleanResults = getBooleanResults(variants, maxRowLength, maxColumnLength, true); return this._aggregateResults(minRange, booleanResults); } From 0839e98d6b109917770f2ec19acf4ac75d267823 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Thu, 27 Jun 2024 16:56:07 +0800 Subject: [PATCH 15/18] fix(formula): averageif supports reference object --- examples/src/sheets/main.ts | 109 ++++++- .../engine/utils/__tests__/compare.spec.ts | 3 + .../src/engine/utils/compare.ts | 4 +- .../src/engine/utils/value-object.ts | 2 +- .../src/functions/information/isref/index.ts | 4 +- .../averageif/__tests__/index.spec.ts | 278 +++++++++++++++--- .../functions/statistical/averageif/index.ts | 46 ++- 7 files changed, 385 insertions(+), 61 deletions(-) diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index 1ece1430cba..8c1b9f10d34 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core'; +import { CellValueType, LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverDocsPlugin } from '@univerjs/docs'; import { UniverDocsUIPlugin } from '@univerjs/docs-ui'; @@ -39,7 +39,6 @@ import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor'; import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'; import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui'; import { enUS, ruRU, zhCN } from '../locales'; -import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo'; /* eslint-disable-next-line node/prefer-global/process */ const IS_E2E: boolean = !!process.env.IS_E2E; @@ -103,7 +102,111 @@ univer.registerPlugin(UniverSheetsDrawingUIPlugin); // create univer sheet instance if (!IS_E2E) { - univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO); + univer.createUnit(UniverInstanceType.UNIVER_SHEET, { + 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, + }, + }, + 5: { + 0: { + v: 'Tom', + t: CellValueType.STRING, + }, + 1: { + v: 'Sarah', + t: CellValueType.STRING, + }, + }, + 6: { + 0: { + v: 'Alex', + t: CellValueType.STRING, + }, + 1: { + v: 'Mickey', + t: CellValueType.STRING, + }, + }, + }, + }, + }, + locale: LocaleType.ZH_CN, + name: '', + sheetOrder: [], + styles: {}, + }); } const mockUser = { 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/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 6bb4899f047..e89e06d5403 100644 --- a/packages/engine-formula/src/engine/utils/value-object.ts +++ b/packages/engine-formula/src/engine/utils/value-object.ts @@ -218,7 +218,7 @@ export function getBooleanResults(variants: BaseValueObject[], maxRowLength: num return booleanResults; } -function removeNonNumberValueObject(array: ArrayValueObject, range: ArrayValueObject) { +export function removeNonNumberValueObject(array: ArrayValueObject, range: ArrayValueObject) { return array.mapValue((valueObject, r, c) => { if (range.get(r, c)?.isNumber()) { return valueObject; 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/statistical/averageif/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts index cf8282d2ada..82d93aff9f0 100644 --- a/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts @@ -14,69 +14,251 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; +import type { Injector } from '@wendellhu/redi'; +import { beforeEach, describe, expect, it } from 'vitest'; -import { ArrayValueObject, transformToValue } from '../../../../engine/value-object/array-value-object'; +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 { StringValueObject } from '../../../../engine/value-object/primitive-object'; +import { ErrorType } from '../../../../basics/error-type'; -describe('Test averageif function', () => { - const testFunction = new Averageif(FUNCTION_NAMES_STATISTICAL.AVERAGEIF); +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 range = ArrayValueObject.create(/*ts*/ `{ - 1; - 4; - 44; - 444 - }`); + 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)'); - const criteria = StringValueObject.create('>40'); + expect(result).toBe(1); + }); + + it('Range string', async () => { + const result = await calculate('=AVERAGEIF("test",">1")'); - const resultObject = testFunction.calculate(range, criteria); - expect(resultObject.getValue()).toBe(244); + expect(result).toBe(ErrorType.DIV_BY_ZERO); + }); + + 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 range = ArrayValueObject.create(/*ts*/ `{ - Ada; - test1; - test12; - Univer - }`); - - const criteria = StringValueObject.create('test*'); - - const sumRange = ArrayValueObject.create(/*ts*/ `{ - 1; - 1; - 1; - 1 - }`); - - const resultObject = testFunction.calculate(range, criteria, sumRange); - expect(resultObject.getValue()).toBe(1); + const result = await calculate('=AVERAGEIF(D4:D6,"test*",A1)'); + + expect(result).toBe(2); }); 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([[4], [4], [44], [444]]); + const result = await calculate('=AVERAGEIF(A3:F4,A3:F4)'); + + expect(result).toBe([[1, 0, 1.23, ErrorType.DIV_BY_ZERO, ErrorType.DIV_BY_ZERO, 0], [0, 100, 2.34, ErrorType.DIV_BY_ZERO, -3, 0]]); }); }); }); diff --git a/packages/engine-formula/src/functions/statistical/averageif/index.ts b/packages/engine-formula/src/functions/statistical/averageif/index.ts index d8b2496f8d5..5b0fe890c24 100644 --- a/packages/engine-formula/src/functions/statistical/averageif/index.ts +++ b/packages/engine-formula/src/functions/statistical/averageif/index.ts @@ -15,7 +15,9 @@ */ import { ErrorType } from '../../../basics/error-type'; -import { valueObjectCompare } from '../../../engine/utils/object-compare'; +import type { BaseReferenceObject, FunctionVariantType } from '../../../engine/reference-object/base-reference-object'; +import { isNumericComparison, valueObjectCompare } from '../../../engine/utils/object-compare'; +import { removeNonNumberValueObject } 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'; @@ -25,24 +27,56 @@ export class Averageif extends BaseFunction { override maxParams = 3; - override calculate(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { - if (range.isError() || criteria.isError() || averageRange?.isError()) { - return ErrorValueObject.create(ErrorType.NA); + 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 (criteria.isReferenceObject()) { + criteria = (criteria as BaseReferenceObject).toArrayValueObject(); + } + + if (averageRange?.isReferenceObject()) { + averageRange = (averageRange as BaseReferenceObject).toArrayValueObject(); + } + + range = range as BaseValueObject; + criteria = criteria as BaseValueObject; + averageRange = averageRange as BaseValueObject; + if (!range.isArray() || (averageRange && !averageRange.isArray())) { return ErrorValueObject.create(ErrorType.VALUE); } if (criteria.isArray()) { - return criteria.map((criteriaItem) => this._handleSingleObject(range, criteriaItem, averageRange)); + return (criteria as ArrayValueObject).map((criteriaItem) => this._handleSingleObject(range, criteriaItem, averageRange)); } return this._handleSingleObject(range, criteria, averageRange); } private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { - const resultArrayObject = valueObjectCompare(range, criteria); + let resultArrayObject = valueObjectCompare(range, criteria); + + // When comparing non-numbers and numbers, it does not take the result + const isNumeric = isNumericComparison(criteria.getValue()); + if (isNumeric) { + resultArrayObject = removeNonNumberValueObject(resultArrayObject as ArrayValueObject, range as ArrayValueObject); + } // averageRange has the same dimensions as range const averageRangeArray = averageRange From 0fc3102460a88aaa879d3187272b3eebc0e4a417 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Thu, 27 Jun 2024 21:00:59 +0800 Subject: [PATCH 16/18] fix(formula): averageif referance --- .../averageif/__tests__/index.spec.ts | 6 ++ .../functions/statistical/averageif/index.ts | 67 +++++++++++++++---- 2 files changed, 60 insertions(+), 13 deletions(-) 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 index 82d93aff9f0..36c3b4a3484 100644 --- a/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts @@ -243,6 +243,12 @@ describe('Test isref function', () => { 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")'); diff --git a/packages/engine-formula/src/functions/statistical/averageif/index.ts b/packages/engine-formula/src/functions/statistical/averageif/index.ts index 5b0fe890c24..c6d5f41a1c3 100644 --- a/packages/engine-formula/src/functions/statistical/averageif/index.ts +++ b/packages/engine-formula/src/functions/statistical/averageif/index.ts @@ -16,6 +16,7 @@ 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 { isNumericComparison, valueObjectCompare } from '../../../engine/utils/object-compare'; import { removeNonNumberValueObject } from '../../../engine/utils/value-object'; import type { ArrayValueObject } from '../../../engine/value-object/array-value-object'; @@ -46,21 +47,21 @@ export class Averageif extends BaseFunction { 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?.isReferenceObject()) { - averageRange = (averageRange as BaseReferenceObject).toArrayValueObject(); + if (averageRange && !averageRange?.isReferenceObject()) { + return ErrorValueObject.create(ErrorType.NA); } range = range as BaseValueObject; criteria = criteria as BaseValueObject; - averageRange = averageRange as BaseValueObject; - - if (!range.isArray() || (averageRange && !averageRange.isArray())) { - return ErrorValueObject.create(ErrorType.VALUE); - } + averageRange = averageRange as BaseReferenceObject; if (criteria.isArray()) { return (criteria as ArrayValueObject).map((criteriaItem) => this._handleSingleObject(range, criteriaItem, averageRange)); @@ -69,30 +70,70 @@ export class Averageif extends BaseFunction { return this._handleSingleObject(range, criteria, averageRange); } - private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseValueObject) { + private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject, averageRange?: BaseReferenceObject) { let resultArrayObject = valueObjectCompare(range, criteria); // When comparing non-numbers and numbers, it does not take the result const isNumeric = isNumericComparison(criteria.getValue()); if (isNumeric) { + // if(resultArrayObject.isArray()){ resultArrayObject = removeNonNumberValueObject(resultArrayObject as ArrayValueObject, range as ArrayValueObject); + // }else{ + // resultArrayObject = range.isNumber() ? resultArrayObject : BooleanValueObject.create(false);; + // } } // averageRange has the same dimensions as range - const averageRangeArray = averageRange - ? (averageRange as ArrayValueObject).slice( - [0, (range as ArrayValueObject).getRowCount()], - [0, (range as ArrayValueObject).getColumnCount()] - ) + 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); + } } From 7c0a1035a9279d94606fe4da2d67f543ce3482ad Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Fri, 28 Jun 2024 21:17:55 +0800 Subject: [PATCH 17/18] fix(formula): countif test --- examples/src/sheets/main.ts | 109 +----- .../utils/__tests__/object-compare.spec.ts | 33 +- .../src/engine/utils/object-compare.ts | 14 - .../src/engine/utils/value-object.ts | 52 ++- .../engine/value-object/primitive-object.ts | 65 ++-- .../math/sumif/__tests__/index.spec.ts | 15 + .../src/functions/math/sumif/index.ts | 9 +- .../meta/compare/__tests__/index.spec.ts | 310 +++++++++++++++++- .../averageif/__tests__/index.spec.ts | 9 +- .../functions/statistical/averageif/index.ts | 16 +- .../countif/__tests__/index.spec.ts | 77 ++++- .../functions/statistical/countif/index.ts | 13 +- .../countifs/__tests__/index.spec.ts | 27 ++ 13 files changed, 557 insertions(+), 192 deletions(-) diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index 8c1b9f10d34..1ece1430cba 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CellValueType, LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core'; +import { LocaleType, LogLevel, Univer, UniverInstanceType, UserManagerService } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverDocsPlugin } from '@univerjs/docs'; import { UniverDocsUIPlugin } from '@univerjs/docs-ui'; @@ -39,6 +39,7 @@ import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor'; import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'; import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui'; import { enUS, ruRU, zhCN } from '../locales'; +import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo'; /* eslint-disable-next-line node/prefer-global/process */ const IS_E2E: boolean = !!process.env.IS_E2E; @@ -102,111 +103,7 @@ univer.registerPlugin(UniverSheetsDrawingUIPlugin); // create univer sheet instance if (!IS_E2E) { - univer.createUnit(UniverInstanceType.UNIVER_SHEET, { - 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, - }, - }, - 5: { - 0: { - v: 'Tom', - t: CellValueType.STRING, - }, - 1: { - v: 'Sarah', - t: CellValueType.STRING, - }, - }, - 6: { - 0: { - v: 'Alex', - t: CellValueType.STRING, - }, - 1: { - v: 'Mickey', - t: CellValueType.STRING, - }, - }, - }, - }, - }, - locale: LocaleType.ZH_CN, - name: '', - sheetOrder: [], - styles: {}, - }); + univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO); } const mockUser = { 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 6de5af925c9..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 { isNumericComparison, valueObjectCompare } from '../object-compare'; +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,16 +210,23 @@ describe('Test object compare', () => { expect(value.getValue()).toStrictEqual(result[i]); }); }); - it('Function isNumericComparison', () => { - expect(isNumericComparison('>40')).toBe(true); - expect(isNumericComparison('<=100')).toBe(true); - expect(isNumericComparison('=5')).toBe(true); - expect(isNumericComparison('test*')).toBe(false); - expect(isNumericComparison('=test')).toBe(false); - expect(isNumericComparison('> 40')).toBe(true); - expect(isNumericComparison('>=3.14')).toBe(true); - expect(isNumericComparison(5)).toBe(true); - expect(isNumericComparison(true)).toBe(false); + 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/object-compare.ts b/packages/engine-formula/src/engine/utils/object-compare.ts index 8222a009341..47f013c4ee8 100644 --- a/packages/engine-formula/src/engine/utils/object-compare.ts +++ b/packages/engine-formula/src/engine/utils/object-compare.ts @@ -94,17 +94,3 @@ export function booleanObjectIntersection(valueObject1: BaseValueObject, valueOb return BooleanValueObject.create(false); }); } - -export function isNumericComparison(condition: string | number | boolean): boolean { - if (typeof condition === 'number') { - return true; - } else if (typeof condition === 'boolean') { - return false; - } - - // Combined regular expression for numeric comparisons - const numericComparisonPattern = /^[<>]?=?\s*\d+(\.\d+)?$/; - - // Test the condition against the pattern - return numericComparisonPattern.test(condition.trim()); -} diff --git a/packages/engine-formula/src/engine/utils/value-object.ts b/packages/engine-formula/src/engine/utils/value-object.ts index e89e06d5403..af0098a002e 100644 --- a/packages/engine-formula/src/engine/utils/value-object.ts +++ b/packages/engine-formula/src/engine/utils/value-object.ts @@ -23,7 +23,7 @@ import { ErrorValueObject } from '../value-object/base-value-object'; import { BooleanValueObject, NumberValueObject } from '../value-object/primitive-object'; import { ErrorType } from '../../basics/error-type'; import { expandArrayValueObject } from './array-object'; -import { booleanObjectIntersection, isNumericComparison, valueObjectCompare } from './object-compare'; +import { booleanObjectIntersection, findCompareToken, valueObjectCompare } from './object-compare'; export function convertTonNumber(valueObject: BaseValueObject) { const currentValue = valueObject.getValue(); @@ -196,10 +196,11 @@ export function getBooleanResults(variants: BaseValueObject[], maxRowLength: num // 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 - const isNumeric = isNumericComparison(criteriaValueObject.getValue()); - if (isNumberSensitive && isNumeric) { - resultArrayObject = removeNonNumberValueObject(resultArrayObject as ArrayValueObject, range as ArrayValueObject); + if (isNumberSensitive) { + resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); } if (booleanResults[rowIndex] === undefined) { @@ -218,12 +219,51 @@ export function getBooleanResults(variants: BaseValueObject[], maxRowLength: num return booleanResults; } -export function removeNonNumberValueObject(array: ArrayValueObject, range: ArrayValueObject) { +/** + * 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) => { - if (range.get(r, c)?.isNumber()) { + const rangeValueObject = range.get(r, c); + if (rangeValueObject && isSameValueObjectType(rangeValueObject, criteria)) { return valueObject; } 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; + } + + // blank string is same as a blank cell + if (left.isString() && left.getValue() !== '' && right.isString() && right.getValue() !== '') { + return true; + } + + if (left.isBoolean() && right.isBoolean()) { + return true; + } + + if (left.isNull() && right.isNull()) { + return true; + } + + if (left.isError() && right.isError()) { + return true; + } + + return false; +} 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 2c988ab1d4f..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,23 +250,23 @@ 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 { @@ -274,6 +274,9 @@ export class BooleanValueObject extends BaseValueObject { return valueObject.compare(this, reverseCompareOperator(operator)); } + if (valueObject.isNull()) { + return this._convertToNumber().compare(valueObject, operator); + } return this.compareBy(valueObject.getValue(), operator); } @@ -284,7 +287,7 @@ export class BooleanValueObject extends BaseValueObject { result = this._compareString(operator); } else if (typeof value === 'boolean') { const booleanNumber = NumberValueObject.create(value ? 1 : 0); - return this._convertTonNumber().compare(booleanNumber, operator); + return this._convertToNumber().compare(booleanNumber, operator); } return BooleanValueObject.create(result); @@ -292,10 +295,10 @@ export class BooleanValueObject extends BaseValueObject { private _compareString(operator: compareToken): boolean { switch (operator) { - case compareToken.EQUALS: 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: @@ -304,14 +307,14 @@ export class BooleanValueObject extends BaseValueObject { } 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) { @@ -321,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/math/sumif/__tests__/index.spec.ts b/packages/engine-formula/src/functions/math/sumif/__tests__/index.spec.ts index b5a5a5ebfd9..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 @@ -41,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; diff --git a/packages/engine-formula/src/functions/math/sumif/index.ts b/packages/engine-formula/src/functions/math/sumif/index.ts index e43e2da0db1..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'; @@ -50,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/meta/compare/__tests__/index.spec.ts b/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts index 9aeb5828ca0..65861182e3b 100644 --- a/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/meta/compare/__tests__/index.spec.ts @@ -14,17 +14,26 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import { FUNCTION_NAMES_META } from '../../function-names'; import { Compare } from '../index'; -import { BooleanValueObject, NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object'; +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); - describe('Compoare', () => { + 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); @@ -41,5 +50,300 @@ describe('Test compare function', () => { 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/averageif/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts index 36c3b4a3484..ffb57ea6c46 100644 --- a/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/averageif/__tests__/index.spec.ts @@ -264,7 +264,14 @@ describe('Test isref function', () => { it('ArrayValueObject range and ArrayValueObject criteria', async () => { const result = await calculate('=AVERAGEIF(A3:F4,A3:F4)'); - expect(result).toBe([[1, 0, 1.23, ErrorType.DIV_BY_ZERO, ErrorType.DIV_BY_ZERO, 0], [0, 100, 2.34, ErrorType.DIV_BY_ZERO, -3, 0]]); + // [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 index c6d5f41a1c3..18ff69a0c69 100644 --- a/packages/engine-formula/src/functions/statistical/averageif/index.ts +++ b/packages/engine-formula/src/functions/statistical/averageif/index.ts @@ -17,8 +17,8 @@ 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 { isNumericComparison, valueObjectCompare } from '../../../engine/utils/object-compare'; -import { removeNonNumberValueObject } from '../../../engine/utils/value-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'; @@ -73,15 +73,9 @@ export class Averageif extends BaseFunction { 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 - const isNumeric = isNumericComparison(criteria.getValue()); - if (isNumeric) { - // if(resultArrayObject.isArray()){ - resultArrayObject = removeNonNumberValueObject(resultArrayObject as ArrayValueObject, range as ArrayValueObject); - // }else{ - // resultArrayObject = range.isNumber() ? resultArrayObject : BooleanValueObject.create(false);; - // } - } + resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); // averageRange has the same dimensions as range let averageRangeArray = averageRange @@ -122,7 +116,7 @@ export class Averageif extends BaseFunction { const rowCount = range.isArray() ? (range as ArrayValueObject).getRowCount() : 1; const columnCount = range.isArray() ? (range as ArrayValueObject).getColumnCount() : 1; - if (averageRangeRow === rowCount || averageRangeColumn === columnCount) { + if (averageRangeRow === rowCount && averageRangeColumn === columnCount) { return averageRange; } 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 index ed960c1b4ff..ecbdfe5572a 100644 --- a/packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/countif/__tests__/index.spec.ts @@ -16,10 +16,11 @@ import { describe, expect, it } from 'vitest'; -import { ArrayValueObject, transformToValue } from '../../../../engine/value-object/array-value-object'; +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); @@ -54,6 +55,18 @@ describe('Test countif function', () => { 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; @@ -72,5 +85,67 @@ describe('Test countif function', () => { 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 index 7b59e4da3c2..3494c497199 100644 --- a/packages/engine-formula/src/functions/statistical/countif/index.ts +++ b/packages/engine-formula/src/functions/statistical/countif/index.ts @@ -15,7 +15,8 @@ */ import { ErrorType } from '../../../basics/error-type'; -import { isNumericComparison, 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'; @@ -42,11 +43,13 @@ export class Countif extends BaseFunction { } private _handleSingleObject(range: BaseValueObject, criteria: BaseValueObject) { - const resultArrayObject = valueObjectCompare(range, criteria); + let resultArrayObject = valueObjectCompare(range, criteria); - const picked = (range as ArrayValueObject).pick(resultArrayObject as ArrayValueObject); + const [, criteriaStringObject] = findCompareToken(`${criteria.getValue()}`); // If the condition is a numeric comparison, only numbers are counted, otherwise text is counted. - const isNumeric = isNumericComparison(criteria.getValue()); - return isNumeric ? picked.count() : picked.countA(); + resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); + + const picked = (range as ArrayValueObject).pick(resultArrayObject as ArrayValueObject); + return picked.countA(); } } 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 index e43921b2d81..b0c8c8c16f5 100644 --- a/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/countifs/__tests__/index.spec.ts @@ -209,5 +209,32 @@ describe('Test countifs function', () => { 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]]); + }); }); }); From d0c4477397490e451fc7abc8805220ce6aa757e4 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Sat, 29 Jun 2024 17:36:30 +0800 Subject: [PATCH 18/18] fix(formula): avedev, countif calculation error --- .../src/engine/utils/value-object.ts | 15 +++++++------- .../engine/value-object/array-value-object.ts | 9 ++++++++- .../avedev/__tests__/index.spec.ts | 17 ++++++++++++++++ .../src/functions/statistical/avedev/index.ts | 9 ++++++++- .../countblank/__tests__/index.spec.ts | 20 +++++++++++++++++-- .../functions/statistical/countblank/index.ts | 4 ++++ .../functions/statistical/countif/index.ts | 18 +++++++++++++++-- 7 files changed, 79 insertions(+), 13 deletions(-) diff --git a/packages/engine-formula/src/engine/utils/value-object.ts b/packages/engine-formula/src/engine/utils/value-object.ts index af0098a002e..230c1812516 100644 --- a/packages/engine-formula/src/engine/utils/value-object.ts +++ b/packages/engine-formula/src/engine/utils/value-object.ts @@ -231,6 +231,8 @@ export function filterSameValueObjectResult(array: ArrayValueObject, range: Arra 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); } @@ -248,20 +250,19 @@ export function isSameValueObjectType(left: BaseValueObject, right: BaseValueObj return true; } - // blank string is same as a blank cell - if (left.isString() && left.getValue() !== '' && right.isString() && right.getValue() !== '') { - return true; - } - if (left.isBoolean() && right.isBoolean()) { return true; } - if (left.isNull() && right.isNull()) { + // 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.isError() && right.isError()) { + if (left.isString() && !isLeftBlank && right.isString() && !isRightBlank) { return true; } 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 e254b3192c1..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 @@ -1429,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, @@ -1617,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/functions/statistical/avedev/__tests__/index.spec.ts b/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts index 4045f581511..61fae799520 100644 --- a/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/avedev/__tests__/index.spec.ts @@ -87,6 +87,23 @@ describe('Test avedev function', () => { 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({ diff --git a/packages/engine-formula/src/functions/statistical/avedev/index.ts b/packages/engine-formula/src/functions/statistical/avedev/index.ts index bccaf2c5a87..910b827b267 100644 --- a/packages/engine-formula/src/functions/statistical/avedev/index.ts +++ b/packages/engine-formula/src/functions/statistical/avedev/index.ts @@ -16,10 +16,12 @@ import type { Nullable } from '@univerjs/core'; import type { ArrayValueObject } from '../../../engine/value-object/array-value-object'; -import type { BaseValueObject, ErrorValueObject } from '../../../engine/value-object/base-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; @@ -63,6 +65,11 @@ export class Avedev extends BaseFunction { } } + // 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; 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 index 5ec856a2762..aacf5caf56d 100644 --- a/packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts +++ b/packages/engine-formula/src/functions/statistical/countblank/__tests__/index.spec.ts @@ -28,9 +28,9 @@ describe('Test countblank function', () => { describe('Countblank', () => { it('Range is error', () => { - const range = ErrorValueObject.create(ErrorType.NA); + const range = ErrorValueObject.create(ErrorType.REF); const result = testFunction.calculate(range); - expect(result.getValue()).toBe(0); + expect(result.getValue()).toBe(ErrorType.REF); }); it('Range is number', () => { const range = NumberValueObject.create(1); @@ -74,5 +74,21 @@ describe('Test countblank function', () => { 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 index f6bc70ac6ea..02ee7ea98d5 100644 --- a/packages/engine-formula/src/functions/statistical/countblank/index.ts +++ b/packages/engine-formula/src/functions/statistical/countblank/index.ts @@ -24,6 +24,10 @@ export class Countblank extends BaseFunction { override maxParams = 1; override calculate(variant: BaseValueObject) { + if (variant.isError()) { + return variant; + } + if (variant.getValue() === '' || variant.isNull()) { return NumberValueObject.create(1); } diff --git a/packages/engine-formula/src/functions/statistical/countif/index.ts b/packages/engine-formula/src/functions/statistical/countif/index.ts index 3494c497199..638ea40e7c9 100644 --- a/packages/engine-formula/src/functions/statistical/countif/index.ts +++ b/packages/engine-formula/src/functions/statistical/countif/index.ts @@ -19,6 +19,7 @@ import { findCompareToken, valueObjectCompare } from '../../../engine/utils/obje 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 { @@ -36,7 +37,7 @@ export class Countif extends BaseFunction { } if (criteria.isArray()) { - return criteria.map((criteriaItem) => this._handleSingleObject(range, criteriaItem)); + return criteria.mapValue((criteriaItem) => this._handleSingleObject(range, criteriaItem)); } return this._handleSingleObject(range, criteria); @@ -50,6 +51,19 @@ export class Countif extends BaseFunction { resultArrayObject = filterSameValueObjectResult(resultArrayObject as ArrayValueObject, range as ArrayValueObject, criteriaStringObject); const picked = (range as ArrayValueObject).pick(resultArrayObject as ArrayValueObject); - return picked.countA(); + 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; } }