diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ecffc1c..e2d8caba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added the `AVERAGEIFS` function. [#1732](https://github.com/handsontable/hyperformula/pull/1732) + ## [3.4.0] - 2026-08-10 ### Added diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 830b1c196..f7bb42ca3 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'PRŮMĚR', AVERAGEA: 'AVERAGEA', AVERAGEIF: 'AVERAGEIF', + AVERAGEIFS: 'AVERAGEIFS', BASE: 'BASE', BIN2DEC: 'BIN2DEC', BIN2HEX: 'BIN2HEX', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 90296fd42..a86e58d87 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'MIDDEL', AVERAGEA: 'MIDDELV', AVERAGEIF: 'MIDDEL.HVIS', + AVERAGEIFS: 'MIDDEL.HVISER', BASE: 'BASIS', BIN2DEC: 'BIN.TIL.DEC', BIN2HEX: 'BIN.TIL.HEX', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index daaaa0bf7..bcf011df4 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'MITTELWERT', AVERAGEA: 'MITTELWERTA', AVERAGEIF: 'MITTELWERTWENN', + AVERAGEIFS: 'MITTELWERTWENNS', BASE: 'BASIS', BIN2DEC: 'BININDEZ', BIN2HEX: 'BININHEX', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732c..2eaa4bd9c 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -39,6 +39,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'AVERAGE', AVERAGEA: 'AVERAGEA', AVERAGEIF: 'AVERAGEIF', + AVERAGEIFS: 'AVERAGEIFS', BASE: 'BASE', BIN2DEC: 'BIN2DEC', BIN2HEX: 'BIN2HEX', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 6fea52e4e..687c4ea94 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -38,6 +38,7 @@ export const dictionary: RawTranslationPackage = { AVERAGE: 'PROMEDIO', AVERAGEA: 'PROMEDIOA', AVERAGEIF: 'PROMEDIO.SI', + AVERAGEIFS: 'PROMEDIO.SI.CONJUNTO', BASE: 'BASE', BIN2DEC: 'BIN.A.DEC', BIN2HEX: 'BIN.A.HEX', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index a3b319af2..96c162ef2 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'KESKIARVO', AVERAGEA: 'KESKIARVOA', AVERAGEIF: 'KESKIARVO.JOS', + AVERAGEIFS: 'KESKIARVO.JOS.JOUKKO', BASE: 'PERUS', BIN2DEC: 'BINDES', BIN2HEX: 'BINHEKSA', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index a29109053..4d0839a16 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'MOYENNE', AVERAGEA: 'AVERAGEA', AVERAGEIF: 'MOYENNE.SI', + AVERAGEIFS: 'MOYENNE.SI.ENS', BASE: 'BASE', BIN2DEC: 'BINDEC', BIN2HEX: 'BINHEX', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index fafc2c4b9..cb803b4c9 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'ÁTLAG', AVERAGEA: 'ÁTLAGA', AVERAGEIF: 'ÁTLAGHA', + AVERAGEIFS: 'ÁTLAGHATÖBB', BASE: 'ALAP', BIN2DEC: 'BIN.DEC', BIN2HEX: 'BIN.HEX', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db..4e52987d1 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -39,6 +39,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'RATA-RATA', AVERAGEA: 'RATA-RATAA', AVERAGEIF: 'RATA-RATA.JIKA', + AVERAGEIFS: 'AVERAGEIFS', BASE: 'BASIS', BIN2DEC: 'BIN.DEC', BIN2HEX: 'BIN.HEX', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 40ec9cf41..ee9231450 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'MEDIA', AVERAGEA: 'MEDIA.VALORI', AVERAGEIF: 'MEDIA.SE', + AVERAGEIFS: 'MEDIA.PIÙ.SE', BASE: 'BASE', BIN2DEC: 'BINARIO.DECIMALE', BIN2HEX: 'BINARIO.HEX', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 89c0f85f7..5878a9f42 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'GJENNOMSNITT', AVERAGEA: 'GJENNOMSNITTA', AVERAGEIF: 'GJENNOMSNITTHVIS', + AVERAGEIFS: 'GJENNOMSNITT.HVIS.SETT', BASE: 'BASE', BIN2DEC: 'BINTILDES', BIN2HEX: 'BINTILHEKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 73ef99687..2c395cd3d 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'GEMIDDELDE', AVERAGEA: 'GEMIDDELDEA', AVERAGEIF: 'GEMIDDELDE.ALS', + AVERAGEIFS: 'GEMIDDELDEN.ALS', BASE: 'BASIS', BIN2DEC: 'BIN.N.DEC', BIN2HEX: 'BIN.N.HEX', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 003b0fd64..8268ac46c 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'ŚREDNIA', AVERAGEA: 'ŚREDNIA.A', AVERAGEIF: 'ŚREDNIA.JEŻELI', + AVERAGEIFS: 'ŚREDNIA.WARUNKÓW', BASE: 'PODSTAWA', BIN2DEC: 'DWÓJK.NA.DZIES', BIN2HEX: 'DWÓJK.NA.SZESN', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 408d7f772..a95a904db 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'MÉDIA', AVERAGEA: 'MÉDIAA', AVERAGEIF: 'MÉDIASE', + AVERAGEIFS: 'MÉDIA.SE.S', BASE: 'BASE', BIN2DEC: 'BINADEC', BIN2HEX: 'BINAHEX', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 657b0db37..7e120546a 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'СРЗНАЧ', AVERAGEA: 'СРЗНАЧА', AVERAGEIF: 'СРЗНАЧЕСЛИ', + AVERAGEIFS: 'СРЗНАЧЕСЛИМН', BASE: 'ОСНОВАНИЕ', BIN2DEC: 'ДВ.В.ДЕС', BIN2HEX: 'ДВ.В.ШЕСТН', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f8758008..fe5101ad2 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'MEDEL', AVERAGEA: 'AVERAGEA', AVERAGEIF: 'MEDEL.OM', + AVERAGEIFS: 'MEDEL.OMF', BASE: 'BASE', BIN2DEC: 'BIN.TILL.DEC', BIN2HEX: 'BIN.TILL.HEX', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index dc243600d..76c85418d 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -38,6 +38,7 @@ const dictionary: RawTranslationPackage = { AVERAGE: 'ORTALAMA', AVERAGEA: 'ORTALAMAA', AVERAGEIF: 'EĞERORTALAMA', + AVERAGEIFS: 'ÇOKEĞERORTALAMA', BASE: 'TABAN', BIN2DEC: 'BIN2DEC', BIN2HEX: 'BIN2HEX', diff --git a/src/interpreter/functionMetadata/categories/statistical.ts b/src/interpreter/functionMetadata/categories/statistical.ts index 1752c3eaf..47dc48d22 100644 --- a/src/interpreter/functionMetadata/categories/statistical.ts +++ b/src/interpreter/functionMetadata/categories/statistical.ts @@ -38,6 +38,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=AVERAGEIF(A1:A10, ">5")', '=AVERAGEIF(B1:B10, "apples", C1:C10)'], }, + AVERAGEIFS: { + category: 'Statistical', + shortDescription: 'Returns the arithmetic mean of the cells in a range that meet a set of criteria.', + parameters: [{name: 'average_range', description: 'The range of cells to average.'}, {name: 'criteria_range1', description: 'A range of cells tested against the paired criterion. Further range/criterion pairs can be passed as additional arguments, and only cells meeting every criterion are considered.'}, {name: 'criteria1', description: 'The condition applied to the preceding range, e.g. ">5", "apples", or a cell reference.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=AVERAGEIFS(C1:C10, A1:A10, ">5")', '=AVERAGEIFS(C1:C10, A1:A10, ">5", B1:B10, "apples")'], + }, 'BETA.DIST': { category: 'Statistical', shortDescription: 'Returns the density of the beta distribution.', diff --git a/src/interpreter/plugin/ConditionalAggregationPlugin.ts b/src/interpreter/plugin/ConditionalAggregationPlugin.ts index f2e74e83c..9c7700f64 100644 --- a/src/interpreter/plugin/ConditionalAggregationPlugin.ts +++ b/src/interpreter/plugin/ConditionalAggregationPlugin.ts @@ -99,6 +99,15 @@ export class ConditionalAggregationPlugin extends FunctionPlugin implements Func {argumentType: FunctionArgumentType.RANGE, optionalArg: true}, ], }, + AVERAGEIFS: { + method: 'averageifs', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NOERROR}, + ], + repeatLastArgs: 2, + }, SUMIFS: { method: 'sumifs', parameters: [ @@ -207,6 +216,56 @@ export class ConditionalAggregationPlugin extends FunctionPlugin implements Func return this.runFunction(ast.args, state, this.metadata(functionName), computeFn) } + /** + * Corresponds to AVERAGEIFS(AverageRange, CriterionRange1, Criterion1, ...) + * + * AverageRange is the range whose matching numeric values are averaged. + * Every CriterionRange and Criterion pair adds a condition that must be satisfied. + * + * @param ast + * @param state + */ + public averageifs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + const functionName = 'AVERAGEIFS' + + const composeFunction = (left: AverageResult | CellError, right: AverageResult | CellError): AverageResult | CellError => { + if (left instanceof CellError) { + return left + } else if (right instanceof CellError) { + return right + } else { + return left.compose(right) + } + } + + const computeFn = (values: SimpleRangeValue, ...args: unknown[]) => { + const averageResult = this.computeConditionalAggregationFunction( + values, + args as RawInterpreterValue[], + functionName, + AverageResult.empty, + composeFunction, + (arg) => { + if (arg instanceof CellError) { + return arg + } else if (isExtendedNumber(arg)) { + return AverageResult.single(getRawValue(arg)) + } else { + return AverageResult.empty + } + }, + ) + + if (averageResult instanceof CellError) { + return averageResult + } else { + return averageResult.averageValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + } + } + + return this.runFunction(ast.args, state, this.metadata(functionName), computeFn) + } + /** * Corresponds to COUNTIF(Range, Criterion) *