diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 10deec9982..459c00f8a0 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -426,9 +426,11 @@ Total number of functions: **{{ $page.functionsCount }}** | LOGINV | Returns value of inverse lognormal distribution. | LOGINV(P; Mean; Stddev) | | MAX | Returns the maximum value in a list of arguments. | MAX(Number1; Number2; ...Number30) | | MAXA | Returns the maximum value in a list of arguments. | MAXA(Value1; Value2; ... Value30) | +| MAXIFS | Returns the max of the values of cells in a range that meets multiple criteria in multiple ranges. | MAXIFS(Max_Range ; Criterion_range1 ; Criterion1 [ ; Criterion_range2 ; Criterion2 [;...]]) | | MEDIAN | Returns the median of a set of numbers. | MEDIAN(Number1; Number2; ...Number30) | | MIN | Returns the minimum value in a list of arguments. | MIN(Number1; Number2; ...Number30) | | MINA | Returns the minimum value in a list of arguments. | MINA(Value1; Value2; ... Value30) | +| MINIFS | Returns the min of the values of cells in a range that meets multiple criteria in multiple ranges. | MINIFS(Min_Range ; Criterion_range1 ; Criterion1 [ ; Criterion_range2 ; Criterion2 [;...]]) | | NEGBINOM.DIST | Returns density of negative binomial distribution. | NEGBINOM.DIST(Number1; Number2; Number3; Mode) | | NEGBINOMDIST | Returns density of negative binomial distribution. | NEGBINOMDIST(Number1; Number2; Number3; Mode) | | NORM.DIST | Returns density of normal distribution. | NORM.DIST(X; Mean; Stddev; Mode) | diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 48ebdd6c14..6b131be540 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'POZVYHLEDAT', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'ČÁST', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUTA', MIRR: 'MOD.MÍRA.VÝNOSNOSTI', MMULT: 'SOUČIN.MATIC', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 4ac76d6e41..4088884741 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'SAMMENLIGN', MAX: 'MAKS', MAXA: 'MAKSV', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'MIDT', MIN: 'MIN', MINA: 'MINV', + MINIFS: 'MINIFS', MINUTE: 'MINUT', MIRR: 'MIA', MMULT: 'MPRODUKT', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index d64eb5e857..f23cd00790 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'VERGLEICH', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXWENNS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'TEIL', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINWENNS', MINUTE: 'MINUTE', MIRR: 'QIKV', MMULT: 'MMULT', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 0bcac8642d..461cebaf6e 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -134,12 +134,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'MATCH', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'MID', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUTE', MIRR: 'MIRR', MMULT: 'MMULT', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index d03fe7dc10..21a6933d5f 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -133,12 +133,14 @@ export const dictionary: RawTranslationPackage = { MATCH: 'COINCIDIR', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIANA', MEDIANPOOL: 'MEDIANPOOL', MID: 'EXTRAE', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUTO', MIRR: 'TIRM', MMULT: 'MMULT', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 64e1a7ce9d..208d1c9885 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'VASTINE', MAX: 'MAKS', MAXA: 'MAKSA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAANI', MEDIANPOOL: 'MEDIANPOOL', MID: 'POIMI.TEKSTI', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUUTIT', MIRR: 'MSISÄINEN', MMULT: 'MKERRO', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index b53864195d..654e730321 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'EQUIV', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIANE', MEDIANPOOL: 'MEDIANPOOL', MID: 'STXT', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUTE', MIRR: 'TRIM', MMULT: 'PRODUITMAT', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index 21bec5c80e..bd20a35823 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'HOL.VAN', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIÁN', MEDIANPOOL: 'MEDIANPOOL', MID: 'KÖZÉP', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'PERCEK', MIRR: 'MEGTÉRÜLÉS', MMULT: 'MSZORZAT', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 4ac2e555be..a80806dadb 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'CONFRONTA', MAX: 'MAX', MAXA: 'MAX.VALORI', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIANA', MEDIANPOOL: 'MEDIANPOOL', MID: 'STRINGA.ESTRAI', MIN: 'MIN', MINA: 'MIN.VALORI', + MINIFS: 'MINIFS', MINUTE: 'MINUTO', MIRR: 'TIR.VAR', MMULT: 'MATR.PRODOTTO', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 5a46913a54..e309b0167e 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'SAMMENLIGNE', MAX: 'STØRST', MAXA: 'MAKSA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'DELTEKST', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUTT', MIRR: 'MODIR', MMULT: 'MMULT', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 36e11c2aa3..a88366778f 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'VERGELIJKEN', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'DEEL', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUUT', MIRR: 'GIR', MMULT: 'PRODUCTMAT', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 05e06f87b0..27fab05782 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'PODAJ.POZYCJĘ', MAX: 'MAKS', MAXA: 'MAX.A', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAKS.Z.PULI', MEDIAN: 'MEDIANA', MEDIANPOOL: 'MEDIANA.Z.PULI', MID: 'FRAGMENT.TEKSTU', MIN: 'MIN', MINA: 'MIN.A', + MINIFS: 'MINIFS', MINUTE: 'MINUTA', MIRR: 'MIRR', MMULT: 'MACIERZ.ILOCZYN', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index a20487828e..145769026b 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'CORRESP', MAX: 'MÁXIMO', MAXA: 'MÁXIMOA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MED', MEDIANPOOL: 'MEDIANPOOL', MID: 'EXT.TEXTO', MIN: 'MÍNIMO', MINA: 'MÍNIMOA', + MINIFS: 'MINIFS', MINUTE: 'MINUTO', MIRR: 'MTIR', MMULT: 'MATRIZ.MULT', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 7f645187fe..e934b8389d 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'ПОИСКПОЗ', MAX: 'МАКС', MAXA: 'МАКСА', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'МЕДИАНА', MEDIANPOOL: 'MEDIANPOOL', MID: 'ПСТР', MIN: 'МИН', MINA: 'МИНА', + MINIFS: 'MINIFS', MINUTE: 'МИНУТЫ', MIRR: 'МВСД', MMULT: 'МУМНОЖ', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 06a975029d..b5c85be50a 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'PASSA', MAX: 'MAX', MAXA: 'MAXA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'MEDIAN', MEDIANPOOL: 'MEDIANPOOL', MID: 'EXTEXT', MIN: 'MIN', MINA: 'MINA', + MINIFS: 'MINIFS', MINUTE: 'MINUT', MIRR: 'MODIR', MMULT: 'MMULT', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 1d3fef7ff8..0a373f35f8 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -133,12 +133,14 @@ const dictionary: RawTranslationPackage = { MATCH: 'KAÇINCI', MAX: 'MAK', MAXA: 'MAKA', + MAXIFS: 'MAXIFS', MAXPOOL: 'MAXPOOL', MEDIAN: 'ORTANCA', MEDIANPOOL: 'MEDIANPOOL', MID: 'PARÇAAL', MIN: 'MİN', MINA: 'MİNA', + MINIFS: 'MINIFS', MINUTE: 'DAKİKA', MIRR: 'D_İÇ_VERİM_ORANI', MMULT: 'DÇARP', diff --git a/src/interpreter/plugin/MinMaxIfsPlugin.ts b/src/interpreter/plugin/MinMaxIfsPlugin.ts new file mode 100644 index 0000000000..1839afb3b7 --- /dev/null +++ b/src/interpreter/plugin/MinMaxIfsPlugin.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright (c) 2021 Handsoncode. All rights reserved. + */ + +import {CellError, ErrorType} from '../../Cell' +import {ErrorMessage} from '../../error-message' +import {ProcedureAst} from '../../parser' +import {Condition, CriterionFunctionCompute} from '../CriterionFunctionCompute' +import {InterpreterState} from '../InterpreterState' +import {getRawValue, InterpreterValue, RawScalarValue} from '../InterpreterValue' +import {SimpleRangeValue} from '../SimpleRangeValue' +import {ArgumentTypes, FunctionPlugin, FunctionPluginTypecheck} from './FunctionPlugin' + +/** Computes key for criterion function cache */ +function minifsCacheKey(conditions: Condition[]): string { + const conditionsStrings = conditions.map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (c) => `${c.conditionRange.range!.sheet},${c.conditionRange.range!.start.col},${c.conditionRange.range!.start.row}` + ) + return ['MINIFS', ...conditionsStrings].join(',') +} + +/** Computes key for criterion function cache */ +function maxifsCacheKey(conditions: Condition[]): string { + const conditionsStrings = conditions.map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (c) => `${c.conditionRange.range!.sheet},${c.conditionRange.range!.start.col},${c.conditionRange.range!.start.row}` + ) + return ['MAXIFS', ...conditionsStrings].join(',') +} + +export class MinMaxIfsPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + public static implementedFunctions = { + MINIFS: { + method: 'minifs', + parameters: [ + {argumentType: ArgumentTypes.RANGE}, + {argumentType: ArgumentTypes.RANGE}, + {argumentType: ArgumentTypes.NOERROR}, + ], + repeatLastArgs: 2, + }, + MAXIFS: { + method: 'maxifs', + parameters: [ + {argumentType: ArgumentTypes.RANGE}, + {argumentType: ArgumentTypes.RANGE}, + {argumentType: ArgumentTypes.NOERROR}, + ], + repeatLastArgs: 2, + }, + } + + public minifs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.compute(ast, state, minifsCacheKey, Number.POSITIVE_INFINITY, (left, right) => Math.min(left, right)) + } + + public maxifs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.compute(ast, state, maxifsCacheKey, Number.NEGATIVE_INFINITY, (left, right) => Math.max(left, right)) + } + + private compute( + ast: ProcedureAst, + state: InterpreterState, + cacheKey: (conditions: Condition[]) => string, + initialValue: RawScalarValue, + fn: (left: number, right: number) => number + ): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('MINIFS'), (values: SimpleRangeValue, ...args) => { + const conditions: Condition[] = [] + for (let i = 0; i < args.length; i += 2) { + const conditionArg = args[i] as SimpleRangeValue + const criterionPackage = this.interpreter.criterionBuilder.fromCellValue(args[i + 1], this.arithmeticHelper) + if (criterionPackage === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.BadCriterion) + } + conditions.push(new Condition(conditionArg, criterionPackage)) + } + + return new CriterionFunctionCompute( + this.interpreter, + cacheKey, + initialValue, + (left, right) => { + if (left instanceof CellError) { + return left + } else if (right instanceof CellError) { + return right + } else if (typeof left === 'number') { + if (typeof right === 'number') { + return fn(left, right) + } else { + return left + } + } else if (typeof right === 'number') { + return right + } else { + return 0 + } + }, + (arg) => getRawValue(arg) + ).compute(values, conditions) + }) + } +} diff --git a/src/interpreter/plugin/index.ts b/src/interpreter/plugin/index.ts index 2704c16e97..003513fd14 100644 --- a/src/interpreter/plugin/index.ts +++ b/src/interpreter/plugin/index.ts @@ -44,3 +44,4 @@ export {StatisticalPlugin} from './StatisticalPlugin' export {MathPlugin} from './MathPlugin' export {ComplexPlugin} from './ComplexPlugin' export {StatisticalAggregationPlugin} from './StatisticalAggregationPlugin' +export {MinMaxIfsPlugin} from './MinMaxIfsPlugin' diff --git a/test/interpreter/function-maxifs.spec.ts b/test/interpreter/function-maxifs.spec.ts new file mode 100644 index 0000000000..cd2932f091 --- /dev/null +++ b/test/interpreter/function-maxifs.spec.ts @@ -0,0 +1,155 @@ +import {HyperFormula} from '../../src' +import {ErrorType} from '../../src/Cell' +import {ErrorMessage} from '../../src/error-message' +import {adr, detailedError, expectArrayWithSameContent} from '../testUtils' + +describe('Function MAXIFS - argument validations and combinations', () => { + it('requires odd number of arguments, but at least 3', () => { + const engine = HyperFormula.buildFromArray([['=MAXIFS(C1, ">0")'], ['=MAXIFS(C1, ">0", B1, B1)']]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + }) + + it('error when criterion unparsable', () => { + const engine = HyperFormula.buildFromArray([ + ['=MAXIFS(B1:B2, C1:C2, "> { + const engine = HyperFormula.buildFromArray([ + ['=MAXIFS(B1:C1, B2:D2, ">0")'], + ['=MAXIFS(B1, B2:D2, ">0")'], + ['=MAXIFS(B1:D1, B2, ">0")'], + ['=MAXIFS(B1:D1, B2:D2, ">0", B2:E2, ">0")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + }) + + it('error when different height dimension of arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['=MAXIFS(B1:B2, C1:C3, ">0")'], + ['=MAXIFS(B1, C1:C2, ">0")'], + ['=MAXIFS(B1:B2, C1, ">0")'], + ['=MAXIFS(B1:B2, C1:C2, ">0", C1:C3, ">0")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + }) + + it('scalars are treated like singular arrays', () => { + const engine = HyperFormula.buildFromArray([['=MAXIFS(42, 10, ">1")'], ['=MAXIFS(42, 0, ">1")']]) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + // because compute() returns the initial value which is Number.NEGATIVE_INFINITY and this isNumberOverflow + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.NUM, ErrorMessage.NaN)) + }) + + it('error propagation', () => { + const engine = HyperFormula.buildFromArray([ + ['=MAXIFS(4/0, 42, ">1")'], + ['=MAXIFS(0, 4/0, ">1")'], + ['=MAXIFS(0, 42, 4/0)'], + ['=MAXIFS(0, 4/0, FOOBAR())'], + ['=MAXIFS(4/0, FOOBAR(), ">1")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A5'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + }) + + it('works when arguments are just references', () => { + const engine = HyperFormula.buildFromArray([['2', '3'], ['=MAXIFS(B1, A1, ">1")']]) + + expect(engine.getCellValue(adr('A2'))).toEqual(3) + }) + + it('works with range values', () => { + const engine = HyperFormula.buildFromArray([ + ['1', '1', '3', '5'], + ['1', '1', '7', '9'], + ['=MAXIFS(MMULT(C1:D2, C1:D2), MMULT(A1:B2, A1:B2), "=2")'], + ['=MAXIFS(MMULT(C1:D2, C1:D2), A1:B2, "=1")'], + ['=MAXIFS(C1:D2, MMULT(A1:B2, A1:B2), "=2")'], + ]) + + expect(engine.getCellValue(adr('A3'))).toEqual(116) + expect(engine.getCellValue(adr('A4'))).toEqual(116) + expect(engine.getCellValue(adr('A5'))).toEqual(9) + }) + + it('works for mixed reference/range arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['2', '3'], + ['=MAXIFS(B1, A1:A1, ">1")'], + ['4', '5'], + ['=MAXIFS(B3:B3, A3, ">1")'], + ]) + + expect(engine.getCellValue(adr('A2'))).toEqual(3) + expect(engine.getCellValue(adr('A4'))).toEqual(5) + }) + + it('works when criterion arg is an inline array', () => { + const engine = HyperFormula.buildFromArray([[2, 'a'], [3, 'b'], ['=MAXIFS(A1:A2, B1:B2, {"a", "b"})']], { + useArrayArithmetic: true, + }) + + expect(engine.getCellValue(adr('A3'))).toEqual(2) + expect(engine.getCellValue(adr('B3'))).toEqual(3) + }) +}) + +describe('Function MAXIFS - calcultions on more than one criteria', () => { + it('works for more than one criterion/range pair', () => { + const engine = HyperFormula.buildFromArray([ + ['0', '100', '3'], + ['1', '101', '5'], + ['2', '102', '7'], + ['=MAXIFS(C1:C3, A1:A3, ">=1", B1:B3, "<102")'], + ]) + + expect(engine.getCellValue(adr('A4'))).toEqual(5) + }) +}) + +describe('Function MAXIFS - cache recalculation after cruds', () => { + it('recalculates MAXIFS if changes in summed range', () => { + const sheet = [['10', '10'], ['5', '6'], ['7', '8'], ['=MAXIFS(A1:B1, A2:B2, ">=5", A3:B3, ">=7")']] + const engine = HyperFormula.buildFromArray(sheet) + + const changes = engine.setCellContents(adr('A1'), [['1', '3']]) + + expect(engine.getCellValue(adr('A4'))).toEqual(3) + expect(changes.length).toEqual(3) + expectArrayWithSameContent( + changes.map((change) => change.newValue), + [1, 3, 4] + ) + }) + + it('recalculates MAXIFS if changes in one of the tested range', () => { + const sheet = [['20', '10'], ['5', '6'], ['7', '8'], ['=MAXIFS(A1:B1, A2:B2, ">=5", A3:B3, ">=7")']] + const engine = HyperFormula.buildFromArray(sheet) + expect(engine.getCellValue(adr('A4'))).toEqual(20) + + engine.setCellContents(adr('A3'), [['1', '7']]) + + expect(engine.getCellValue(adr('A4'))).toEqual(10) + }) +}) diff --git a/test/interpreter/function-minifs.spec.ts b/test/interpreter/function-minifs.spec.ts new file mode 100644 index 0000000000..9fca20db5d --- /dev/null +++ b/test/interpreter/function-minifs.spec.ts @@ -0,0 +1,155 @@ +import {HyperFormula} from '../../src' +import {ErrorType} from '../../src/Cell' +import {ErrorMessage} from '../../src/error-message' +import {adr, detailedError, expectArrayWithSameContent} from '../testUtils' + +describe('Function MINIFS - argument validations and combinations', () => { + it('requires odd number of arguments, but at least 3', () => { + const engine = HyperFormula.buildFromArray([['=MINIFS(C1, ">0")'], ['=MINIFS(C1, ">0", B1, B1)']]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + }) + + it('error when criterion unparsable', () => { + const engine = HyperFormula.buildFromArray([ + ['=MINIFS(B1:B2, C1:C2, "> { + const engine = HyperFormula.buildFromArray([ + ['=MINIFS(B1:C1, B2:D2, ">0")'], + ['=MINIFS(B1, B2:D2, ">0")'], + ['=MINIFS(B1:D1, B2, ">0")'], + ['=MINIFS(B1:D1, B2:D2, ">0", B2:E2, ">0")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + }) + + it('error when different height dimension of arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['=MINIFS(B1:B2, C1:C3, ">0")'], + ['=MINIFS(B1, C1:C2, ">0")'], + ['=MINIFS(B1:B2, C1, ">0")'], + ['=MINIFS(B1:B2, C1:C2, ">0", C1:C3, ">0")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.EqualLength)) + }) + + it('scalars are treated like singular arrays', () => { + const engine = HyperFormula.buildFromArray([['=MINIFS(42, 10, ">1")'], ['=MINIFS(42, 0, ">1")']]) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + // because compute() returns the initial value which is Number.POSITIVE_INFINITY and this isNumberOverflow + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.NUM, ErrorMessage.NaN)) + }) + + it('error propagation', () => { + const engine = HyperFormula.buildFromArray([ + ['=MINIFS(4/0, 42, ">1")'], + ['=MINIFS(0, 4/0, ">1")'], + ['=MINIFS(0, 42, 4/0)'], + ['=MINIFS(0, 4/0, FOOBAR())'], + ['=MINIFS(4/0, FOOBAR(), ">1")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A5'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + }) + + it('works when arguments are just references', () => { + const engine = HyperFormula.buildFromArray([['2', '3'], ['=MINIFS(B1, A1, ">1")']]) + + expect(engine.getCellValue(adr('A2'))).toEqual(3) + }) + + it('works with range values', () => { + const engine = HyperFormula.buildFromArray([ + ['1', '1', '3', '5'], + ['1', '1', '7', '9'], + ['=MINIFS(MMULT(C1:D2, C1:D2), MMULT(A1:B2, A1:B2), "=2")'], + ['=MINIFS(MMULT(C1:D2, C1:D2), A1:B2, "=1")'], + ['=MINIFS(C1:D2, MMULT(A1:B2, A1:B2), "=2")'], + ]) + + expect(engine.getCellValue(adr('A3'))).toEqual(44) + expect(engine.getCellValue(adr('A4'))).toEqual(44) + expect(engine.getCellValue(adr('A5'))).toEqual(3) + }) + + it('works for mixed reference/range arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['2', '3'], + ['=MINIFS(B1, A1:A1, ">1")'], + ['4', '5'], + ['=MINIFS(B3:B3, A3, ">1")'], + ]) + + expect(engine.getCellValue(adr('A2'))).toEqual(3) + expect(engine.getCellValue(adr('A4'))).toEqual(5) + }) + + it('works when criterion arg is an inline array', () => { + const engine = HyperFormula.buildFromArray([[2, 'a'], [3, 'b'], ['=MINIFS(A1:A2, B1:B2, {"a", "b"})']], { + useArrayArithmetic: true, + }) + + expect(engine.getCellValue(adr('A3'))).toEqual(2) + expect(engine.getCellValue(adr('B3'))).toEqual(3) + }) +}) + +describe('Function MINIFS - calcultions on more than one criteria', () => { + it('works for more than one criterion/range pair', () => { + const engine = HyperFormula.buildFromArray([ + ['0', '100', '3'], + ['1', '101', '5'], + ['2', '102', '7'], + ['=MINIFS(C1:C3, A1:A3, ">=1", B1:B3, "<102")'], + ]) + + expect(engine.getCellValue(adr('A4'))).toEqual(5) + }) +}) + +describe('Function MINIFS - cache recalculation after cruds', () => { + it('recalculates MINIFS if changes in summed range', () => { + const sheet = [['10', '10'], ['5', '6'], ['7', '8'], ['=MINIFS(A1:B1, A2:B2, ">=5", A3:B3, ">=7")']] + const engine = HyperFormula.buildFromArray(sheet) + + const changes = engine.setCellContents(adr('A1'), [['1', '3']]) + + expect(engine.getCellValue(adr('A4'))).toEqual(1) + expect(changes.length).toEqual(3) + expectArrayWithSameContent( + changes.map((change) => change.newValue), + [1, 3, 4] + ) + }) + + it('recalculates MINIFS if changes in one of the tested range', () => { + const sheet = [['10', '20'], ['5', '6'], ['7', '8'], ['=MINIFS(A1:B1, A2:B2, ">=5", A3:B3, ">=7")']] + const engine = HyperFormula.buildFromArray(sheet) + expect(engine.getCellValue(adr('A4'))).toEqual(10) + + engine.setCellContents(adr('A3'), [['1', '7']]) + + expect(engine.getCellValue(adr('A4'))).toEqual(20) + }) +})