Skip to content

Commit

Permalink
feat(formula): add choose function (#2613)
Browse files Browse the repository at this point in the history
* feat(formula): add choose function

* fix(formula): numfmt formula sum

* fix(formula): choose function ref 0

* fix(formula): choose return reference object
  • Loading branch information
Dushusir committed Jun 28, 2024
1 parent ae4670a commit 219a053
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,8 @@ describe('Test numfmt kit', () => {
expect(comparePatternPriority(numfmtMap.date, numfmtMap.date, operatorToken.MULTIPLY)).toBe('');
// Date / Date = General
expect(comparePatternPriority(numfmtMap.date, numfmtMap.date, operatorToken.DIVIDED)).toBe('');

expect(comparePatternPriority(numfmtMap.date, '', operatorToken.PLUS)).toBe(numfmtMap.date);
expect(comparePatternPriority('', numfmtMap.accounting, operatorToken.MINUS)).toBe(numfmtMap.accounting);
});
});
6 changes: 6 additions & 0 deletions packages/engine-formula/src/engine/utils/numfmt-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ function isAccounting(pattern: string) {
* @param nextPattern
*/
export function comparePatternPriority(previousPattern: string, nextPattern: string, operator: operatorToken) {
if (previousPattern === '') {
return nextPattern;
} else if (nextPattern === '') {
return previousPattern;
}

const previousPatternType = getNumberFormatType(previousPattern);
const nextPatternType = getNumberFormatType(nextPattern);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,9 @@ export class NumberValueObject extends BaseValueObject {
if (valueObject.isArray()) {
return valueObject.plus(this);
}

const pattern = comparePatternPriority(this.getPattern(), valueObject.getPattern(), operatorToken.PLUS);

const object = this.plusBy(valueObject.getValue());

// = 1 + #NAME? gets #NAME?, = 1 + #VALUE! gets #VALUE!
Expand All @@ -441,7 +444,7 @@ export class NumberValueObject extends BaseValueObject {
}

// Set number format
object.setPattern(comparePatternPriority(this.getPattern(), valueObject.getPattern(), operatorToken.PLUS));
object.setPattern(pattern);

return object;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ import { FUNCTION_NAMES_STATISTICAL } from '../statistical/function-names';
import { FUNCTION_NAMES_DATE } from '../date/function-names';
import { FUNCTION_NAMES_TEXT } from '../text/function-names';
import { IFunctionService } from '../../services/function.service';
import type { ArrayValueObject } from '../../engine/value-object/array-value-object';
import type { LexerNode } from '../../engine/analysis/lexer-node';
import { Choose } from '../lookup/choose';
import { Sum } from '../math/sum';
import { ErrorType } from '../../basics/error-type';
import { createFunctionTestBed, getObjectValue } from './create-function-test-bed';

const getFunctionsTestWorkbookData = (): IWorkbookData => {
Expand Down Expand Up @@ -252,6 +254,7 @@ describe('Test nested functions', () => {
let lexer: Lexer;
let astTreeBuilder: AstTreeBuilder;
let interpreter: Interpreter;
let calculate: (formula: string) => (string | number | boolean | null)[][] | string | number | boolean;

beforeEach(() => {
const testBed = createFunctionTestBed(getFunctionsTestWorkbookData());
Expand Down Expand Up @@ -307,29 +310,55 @@ describe('Test nested functions', () => {
new Min(FUNCTION_NAMES_STATISTICAL.MIN),
new Plus(FUNCTION_NAMES_META.PLUS),
new Minus(FUNCTION_NAMES_META.MINUS),
new Concatenate(FUNCTION_NAMES_TEXT.CONCATENATE)
new Concatenate(FUNCTION_NAMES_TEXT.CONCATENATE),
new Sum(FUNCTION_NAMES_MATH.SUM),
new Choose(FUNCTION_NAMES_LOOKUP.CHOOSE)

);
});

describe('Normal', () => {
it('Nested functions IFERROR,XLOOKUP,MAX,SUMIFS,EDATE,TODAY,DAY,PLUS,Minus,CONCATENATE', () => {
const lexerNode = lexer.treeBuilder('=IFERROR(XLOOKUP(MAX(SUMIFS(C2:C10, A2:A10, ">="&EDATE(TODAY(),-1)+1-DAY(TODAY()), A2:A10, "<"&TODAY()-DAY(TODAY())+1)), SUMIFS(C2:C10, A2:A10, ">="&EDATE(TODAY(),-1)+1-DAY(TODAY()), A2:A10, "<"&TODAY()-DAY(TODAY())+1), B2:B10, "No Data"), "No Data")');
calculate = (formula: string) => {
const lexerNode = lexer.treeBuilder(formula);

const astNode = astTreeBuilder.parse(lexerNode as LexerNode);

const result = interpreter.execute(astNode as BaseAstNode);

expect((result as ArrayValueObject).toValue()).toStrictEqual([[101], [102], [103], [104], [105], [101], [102], [103], [104]]);
return getObjectValue(result);
};
});

describe('Normal', () => {
it('Nested functions IFERROR,XLOOKUP,MAX,SUMIFS,EDATE,TODAY,DAY,PLUS,Minus,CONCATENATE', () => {
const result = calculate('=IFERROR(XLOOKUP(MAX(SUMIFS(C2:C10, A2:A10, ">="&EDATE(TODAY(),-1)+1-DAY(TODAY()), A2:A10, "<"&TODAY()-DAY(TODAY())+1)), SUMIFS(C2:C10, A2:A10, ">="&EDATE(TODAY(),-1)+1-DAY(TODAY()), A2:A10, "<"&TODAY()-DAY(TODAY())+1), B2:B10, "No Data"), "No Data")');

expect(result).toStrictEqual([[101], [102], [103], [104], [105], [101], [102], [103], [104]]);
});
it('Nested functions ADDRESS,XMATCH,MIN,SUMIFS,EDATE,TODAY,DAY', () => {
const lexerNode = lexer.treeBuilder('=ADDRESS(XMATCH(MIN(SUMIFS(C2:C10, A2:A10, ">=" & EDATE(TODAY(), -1) + 1 - DAY(TODAY()), A2:A10, "<" & TODAY() - DAY(TODAY()) + 1)), SUMIFS(C2:C10, A2:A10, ">=" & EDATE(TODAY(), -1) + 1 - DAY(TODAY()), A2:A10, "<" & TODAY() - DAY(TODAY()) + 1), 0) + 1, 2)');
const result = calculate('=ADDRESS(XMATCH(MIN(SUMIFS(C2:C10, A2:A10, ">=" & EDATE(TODAY(), -1) + 1 - DAY(TODAY()), A2:A10, "<" & TODAY() - DAY(TODAY()) + 1)), SUMIFS(C2:C10, A2:A10, ">=" & EDATE(TODAY(), -1) + 1 - DAY(TODAY()), A2:A10, "<" & TODAY() - DAY(TODAY()) + 1), 0) + 1, 2)');

const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
expect(result).toStrictEqual([['$B$2']]);
});

const result = interpreter.execute(astNode as BaseAstNode);
it('SUM, CHOOSE', () => {
let result = calculate('=SUM(A2:CHOOSE(2,B2,C2))');

expect(result).toStrictEqual(45033);

result = calculate('=SUM(CHOOSE(1,A2:B2))');

expect(result).toStrictEqual(45028);

result = calculate('=SUM(CHOOSE({1,1},A2:B2))');

expect(result).toStrictEqual(45028);

result = calculate('=SUM(CHOOSE({1,1,1},A2:B2))');

expect(result).toStrictEqual(ErrorType.NA);

result = calculate('=SUM(CHOOSE({1,2},A2:B2))');

expect(getObjectValue(result)).toStrictEqual([['$B$2']]);
expect(result).toStrictEqual(ErrorType.VALUE);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* 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, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { FUNCTION_NAMES_LOOKUP } from '../../function-names';
import { Choose } from '../index';
import { NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object';
import { ErrorType } from '../../../../basics/error-type';
import { getObjectValue } from '../../../__tests__/create-function-test-bed';
import { ErrorValueObject } from '../../../../engine/value-object/base-value-object';

describe('Test choose function', () => {
const testFunction = new Choose(FUNCTION_NAMES_LOOKUP.CHOOSE);

describe('Choose', () => {
it('Index num and value', async () => {
const indexNum = NumberValueObject.create(2);
const value1 = NumberValueObject.create(11);
const value2 = NumberValueObject.create(22);

const resultObject = testFunction.calculate(indexNum, value1, value2);
expect(getObjectValue(resultObject)).toStrictEqual(22);
});

it('Index num error', async () => {
const indexNum = ErrorValueObject.create(ErrorType.NAME);
const value1 = NumberValueObject.create(11);

const resultObject = testFunction.calculate(indexNum, value1);
expect(getObjectValue(resultObject)).toStrictEqual(ErrorType.NAME);
});

it('Index num and value, exceeding quantity', async () => {
const indexNum = NumberValueObject.create(3);
const value1 = NumberValueObject.create(11);
const value2 = NumberValueObject.create(22);

const resultObject = testFunction.calculate(indexNum, value1, value2);
expect(getObjectValue(resultObject)).toStrictEqual(ErrorType.VALUE);
});

it('Index num array', async () => {
const indexNum = ArrayValueObject.create(/*ts*/ `{
1,2,3
}`);
const value1 = NumberValueObject.create(11);
const value2 = StringValueObject.create('second');

const resultObject = testFunction.calculate(indexNum, value1, value2);
expect(getObjectValue(resultObject)).toStrictEqual([[11, 'second', ErrorType.VALUE]]);
});

it('Index num number, value1 array', async () => {
const indexNum = NumberValueObject.create(1.9);

const value1 = ArrayValueObject.create(/*ts*/ `{
2;
3;
4
}`);
const resultObject = testFunction.calculate(indexNum, value1);
expect(getObjectValue(resultObject)).toStrictEqual([[2], [3], [4]]);
});

it('Index num number negative', async () => {
const indexNum = NumberValueObject.create(-2);

const value1 = ArrayValueObject.create(/*ts*/ `{
2;
3;
4
}`);
const resultObject = testFunction.calculate(indexNum, value1);
expect(getObjectValue(resultObject)).toStrictEqual(ErrorType.VALUE);
});

it('Index num number, value1 array with blank cell', async () => {
const indexNum = NumberValueObject.create(1);

const value1 = ArrayValueObject.create({
calculateValueList: transformToValueObject([
[null],
]),
rowCount: 1,
columnCount: 1,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
const resultObject = testFunction.calculate(indexNum, value1);
expect(getObjectValue(resultObject)).toStrictEqual([[0]]);
});

it('All params with array', async () => {
const indexNum = ArrayValueObject.create(/*ts*/ `{
1;
2;
3
}`);

const value1 = ArrayValueObject.create(/*ts*/ `{
11,22,33,44
}`);

const value2 = ArrayValueObject.create(/*ts*/ `{
44,77;
55,88;
66,99
}`);
const value3 = NumberValueObject.create(3);

const resultObject = testFunction.calculate(indexNum, value1, value2, value3);
expect(getObjectValue(resultObject)).toStrictEqual([[11, 22, 33, 44], [55, 88, ErrorType.NA, ErrorType.NA], [3, 3, 3, 3]]);
});
});
});
105 changes: 105 additions & 0 deletions packages/engine-formula/src/functions/lookup/choose/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ErrorType } from '../../../basics/error-type';
import type { BaseReferenceObject, FunctionVariantType } from '../../../engine/reference-object/base-reference-object';
import { expandArrayValueObject } from '../../../engine/utils/array-object';
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
import { BaseFunction } from '../../base-function';

export class Choose extends BaseFunction {
override minParams = 2;

override maxParams = 255;

override needsReferenceObject = true;

override isAddress() {
return true;
}

override calculate(indexNum: FunctionVariantType, ...variants: FunctionVariantType[]) {
if (indexNum.isError()) {
return indexNum;
}

if (indexNum.isReferenceObject()) {
indexNum = (indexNum as BaseReferenceObject).toArrayValueObject();
}

indexNum = indexNum as BaseValueObject;

if (!indexNum.isArray()) {
const index = indexNum.convertToNumberObjectValue();

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

const variant = variants[Math.trunc(+index.getValue()) - 1] || ErrorValueObject.create(ErrorType.VALUE);
// if (variant.isNull()) {
// variant = NumberValueObject.create(0);
// }
return variant;
}

// The size of the extended range is determined by the maximum width and height of the criteria range.
let maxRowLength = indexNum.isArray() ? (indexNum as ArrayValueObject).getRowCount() : 1;
let maxColumnLength = indexNum.isArray() ? (indexNum as ArrayValueObject).getColumnCount() : 1;

variants.forEach((variant, i) => {
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 indexNumArray = expandArrayValueObject(maxRowLength, maxColumnLength, indexNum, ErrorValueObject.create(ErrorType.NA));
const arrayValueObjectList = variants.map((variant) => {
if (variant.isReferenceObject()) {
variant = (variant as BaseReferenceObject).toArrayValueObject();
}
return expandArrayValueObject(maxRowLength, maxColumnLength, variant as BaseValueObject, ErrorValueObject.create(ErrorType.NA));
});

return indexNumArray.map((indexNumValue, row, column) => {
if (indexNumValue.isError()) {
return indexNumValue;
}

const index = indexNumValue.convertToNumberObjectValue();

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

const arrayValueObject = arrayValueObjectList[Math.trunc(+index.getValue()) - 1];

let valueObject = arrayValueObject?.get(row, column) || ErrorValueObject.create(ErrorType.VALUE);
if (valueObject?.isNull()) {
valueObject = NumberValueObject.create(0);
}
return valueObject;
});
}
}
2 changes: 2 additions & 0 deletions packages/engine-formula/src/functions/lookup/function-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ import { Rows } from './rows';
import { Vlookup } from './vlookup';
import { Xlookup } from './xlookup';
import { Xmatch } from './xmatch';
import { Choose } from './choose';
import { Index } from './index';

export const functionLookup = [
[Address, FUNCTION_NAMES_LOOKUP.ADDRESS],
[Choose, FUNCTION_NAMES_LOOKUP.CHOOSE],
[Column, FUNCTION_NAMES_LOOKUP.COLUMN],
[Columns, FUNCTION_NAMES_LOOKUP.COLUMNS],
[Index, FUNCTION_NAMES_LOOKUP.INDEX],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default {
},
},
CHOOSE: {
description: 'Chooses a value from a list of values',
description: 'Chooses a value from a list of values.',
abstract: 'Chooses a value from a list of values',
links: [
{
Expand All @@ -73,8 +73,9 @@ export default {
},
],
functionParameter: {
number1: { name: 'number1', detail: 'first' },
number2: { name: 'number2', detail: 'second' },
indexNum: { name: 'index_num', detail: 'Specifies which value argument is selected. Index_num must be a number between 1 and 254, or a formula or reference to a cell containing a number between 1 and 254.\nIf index_num is 1, CHOOSE returns value1; if it is 2, CHOOSE returns value2; and so on.\nIf index_num is less than 1 or greater than the number of the last value in the list, CHOOSE returns the #VALUE! error value.\nIf index_num is a fraction, it is truncated to the lowest integer before being used.' },
value1: { name: 'value1', detail: 'CHOOSE selects a value or an action to perform based on index_num. The arguments can be numbers, cell references, defined names, formulas, functions, or text.' },
value2: { name: 'value2', detail: '1 to 254 value arguments.' },
},
},
CHOOSECOLS: {
Expand Down
Loading

0 comments on commit 219a053

Please sign in to comment.