Skip to content

Commit

Permalink
feat(formula): add countif
Browse files Browse the repository at this point in the history
  • Loading branch information
Dushusir committed Jun 20, 2024
1 parent 424f702 commit fe2a0c9
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { describe, expect, it } from 'vitest';

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

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

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

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

// Test the condition against the pattern
return numericComparisonPattern.test(condition.trim());
}
Original file line number Diff line number Diff line change
@@ -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
*
* https://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]]);
});
});
});
64 changes: 64 additions & 0 deletions packages/engine-formula/src/functions/statistical/countif/index.ts
Original file line number Diff line number Diff line change
@@ -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
*
* https://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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import { Maxifs } from './maxifs';
import { Minifs } from './minifs';
import { Averageif } from './averageif';
import { Averageifs } from './averageifs';
import { Countif } from './countif';

export const functionStatistical = [
[Average, FUNCTION_NAMES_STATISTICAL.AVERAGE],
[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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,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: [
{
Expand All @@ -342,8 +342,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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,16 +315,16 @@ export default {
},
COUNTIF: {
description: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します。',
abstract: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します',
abstract: '指定された範囲に含まれるセルのうち、検索条件に一致するセルの個数を返します',
links: [
{
title: '指導',
url: 'https://support.microsoft.com/ja-jp/office/countif-%E9%96%A2%E6%95%B0-e0de10c6-f885-4e71-abb4-1f464816df34',
},
],
functionParameter: {
number1: { name: 'number1', detail: 'first' },
number2: { name: 'number2', detail: 'second' },
range: { name: '範囲', detail: '数えるセルのグループ。 範囲には、数値、配列、名前付き範囲、(数値を含む) 参照が入ります。 空の値とテキスト値は無視されます。' },
criteria: { name: '検索条件', detail: '個数の計算対象となるセルを決定する条件を、数値、式、セル参照、または文字列で指定します。\nたとえば、数値として 32、比較演算子として ">32"、セル参照として B4、文字列として "リンゴ" などを指定できます。\nCOUNTIF で指定できるのは、単一の検索条件のみです。 複数の検索条件を指定する場合は、COUNTIFS を使います。' },
},
},
COUNTIFS: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export default {
},
},
COUNTIF: {
description: '计算区域内符合给定条件的单元格的数量',
description: '计算区域内符合给定条件的单元格的数量',
abstract: '计算区域内符合给定条件的单元格的数量',
links: [
{
Expand All @@ -342,8 +342,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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down

0 comments on commit fe2a0c9

Please sign in to comment.