Feature/averageifs function - #1732
Conversation
|
@Tobiadefami thanks for the pull request. No CLA step needed here — our records show you signed the Contributor License Agreement on 2026-07-31. That signature came from our previous signing form and has been carried over, so there is nothing for you to re-sign. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | c6e1fb5 | Commit Preview URL Branch Preview URL |
Sep 04 2026, 08:05 PM |
Performance comparison of head (c6e1fb5) vs base (c920375) |
|
Hey @Tobiadefami — one process thing unrelated to the code itself (which looks solid, see the review above): the paired Could you open a PR there for this branch? Same thing seems to have happened for #1733's companion branch ( Nothing else blocking here — just flagging it now so it's not a surprise closer to merge. |
|
@marcin-kordas-hoc Done — AVERAGEIFS is in test repo PR 36, and the zero-average fix is in test repo PR 35. |
marcin-kordas-hoc
left a comment
There was a problem hiding this comment.
Approving. The engine code here got a clean verdict on 16 Aug and the head has not moved since, so this is about closing the loop on the two things that were still open.
Paired tests now verified against this head. CI on this PR last ran on 13 Aug, while the feature/averageifs-function test branch received commits through 22 Aug — so the green badge above never executed the current test suite. I checked it directly instead: engine 8f54b3ab0 against tests d0f8a4f, function-averageifs.spec.ts → 29/29 passing. The 22 Aug delta (percents, currencies, dates) is structurally the same as the MAXIFS siblings and passes here.
One caveat on the record: I could not re-trigger the workflow — this token has no actions: write, and test.yml has no workflow_dispatch, so the only paths are the re-run button or a new push. Worth one click before merge so the PR's own badge reflects a run that included those tests. My local run is evidence the tests pass, not a substitute for the official record.
Also unable to resolve my own thread on ConditionalAggregationPlugin.ts from here (same token limitation) — your reply on it is correct and it needs no further action, so please mark it resolved.
Merging still waits for the freeze to lift.
|
Flagging this before merge, since the PR ticks "My change is compatible with Microsoft Excel" and there is a test that currently locks the divergence in. Repro (verified against this branch at
Why Excel returns that. The AVERAGEIFS page linked from HF-140 states:
This is one of the spots where Excel deliberately makes
So boolean handling is not something Suggested fix. In the map function at lines 248-256, handle booleans explicitly before the Test that needs updating alongside it. In the paired tests PR, Our preference is to match Excel here, rather than record the current behaviour in Generated by Claude Code |
|
// averageif, lines 209-213 == averageifs, lines 259-263
if (averageResult instanceof CellError) {
return averageResult
} else {
return averageResult.averageValue() ?? new CellError(ErrorType.DIV_BY_ZERO)
}This isn't a hypothetical maintenance argument — the duplication has already cost a PR. Those two blocks used to disagree: To be clear about the scope of what I'm suggesting: not one function serving both. The mapping lambda should stay per-function — as I noted in the boolean-handling comment above, Excel deliberately treats private computeAverage(
values: SimpleRangeValue,
conditionArgs: RawInterpreterValue[],
functionName: string,
mapFn: (arg: InternalScalarValue) => AverageResult | CellError,
): InterpreterValueBoth public methods then reduce to assembling their own arguments and passing their own DEV_DOCS asks for exactly this:
And this file already demonstrates the pattern: Generated by Claude Code |
sequba
left a comment
There was a problem hiding this comment.
One inline note on the Portuguese translation, with a broader ask: the localized names added here should each be confirmed against the sources DEV_DOCS lists, rather than inferred from the neighbouring entries in the same pack — ptPT.ts turns out not to be internally consistent, so its neighbours aren't a safe reference.
Generated by Claude Code
| AVERAGE: 'MÉDIA', | ||
| AVERAGEA: 'MÉDIAA', | ||
| AVERAGEIF: 'MÉDIASE', | ||
| AVERAGEIFS: 'MÉDIA.SE.S', |
There was a problem hiding this comment.
MÉDIA.SE.S doesn't match the naming shape of the rest of this pack — the whole family needs checking against the DEV_DOCS sources
Commit 46ac574 changed this entry from MÉDIASES to MÉDIA.SE.S. The result is that AVERAGEIF and AVERAGEIFS — the same two words, "média" + "se" — are now spelled to two different conventions one line apart:
AVERAGEIF: 'MÉDIASE', // line 40
AVERAGEIFS: 'MÉDIA.SE.S', // line 41The rest of the conditional family in this file:
| Function | ptPT.ts | Function | ptPT.ts |
|---|---|---|---|
SUMIF |
SOMASE |
MAXIFS |
MÁXIMOSES |
SUMIFS |
SOMASES |
MINIFS |
MÍNIMOSES |
COUNTIF |
CONT.SE |
IFS |
SE.S |
COUNTIFS |
CONT.SES |
My read — and this is the part that needs confirming, not taking from me — is that MÉDIASE, SOMASE, SOMASES, MÁXIMOSES and MÍNIMOSES are the pt-BR names, while MÉDIA.SE.S (and SE.S) are the pt-PT ones, so a pack labelled ptPT is mostly populated with Brazilian names. If that's right, this new entry is the correct one and the surrounding pack is what's wrong — which would make this a follow-up ticket on the pack rather than a change to this PR. I'd rather not guess at it either way.
What I'd ask for here: verify every name added in this PR against the sources DEV_DOCS names, rather than deriving them from the neighbouring entries — since, as the table shows, the neighbours are not a reliable guide in this file. DEV_DOCS is specific about where to look:
Only add a localized function name after confirming that Microsoft Excel ships that exact name. If an authoritative source does not provide a localized name, keep the English name instead of translating or inferring one.
with the Functions Translator and dolf.trieschnigg.nl as the primary sources, the localized alphabetical list as the fallback, and this instruction for the fallback path:
Find the link whose URL contains
functions/<english-name>-function, then follow it and confirm that the individual function page uses the same localized name in its title and formula syntax. Check each locale independently because Excel keeps some function names in English.
Two other entries in this PR would benefit from the same check, both because they keep the English name:
csCZ.ts—AVERAGEIFS: 'AVERAGEIFS'. Plausibly correct, since every*IFSin that pack is English.idID.ts—AVERAGEIFS: 'AVERAGEIFS', whileAVERAGEIFisRATA-RATA.JIKA,MAXIFSisMAKS.JIKAS,MINIFSisMIN.JIKASandSUMIFSisJUMLAH.JIKAH. Indonesian isn't an Excel locale, so DEV_DOCS routes it to Google Sheets (https://support.google.com/docs/table/25273?hl=id) and permits the English fallback only "for functions that Google Sheets does not list either." Given theMAKS.JIKAS/MIN.JIKASpattern already in that file, it seems likely Sheets does list a name — but I couldn't reach that page from my environment to confirm, so it needs a human to open it.
The other fourteen look consistent with their packs to me; the ask is just that they've each been checked against a source rather than inferred.
Generated by Claude Code
There was a problem hiding this comment.
Thanks for flagging. I verified all 17 names against the DEV_DOCS.md sources. The entries in this PR are correct, including MÉDIA.SE.S for ptPT and AVERAGEIFS for csCZ and idID.
The surrounding ptPT entries have pre-existing inconsistencies. I’ll address those in a separate follow-up PR to keep this PR scoped to AVERAGEIFS.
sequba
left a comment
There was a problem hiding this comment.
Three code-quality notes on the new averageifs method, grouped into one review since they all sit within lines 220-256 and none of them blocks the PR:
- Line 220 — the JSDoc names the arguments
AverageRange/CriterionRange1/Criterion1, while this same PR publishesaverage_range/criteria_range1/criteria1throughgetFunctionDetailsand the generated docs page. - Line 241 —
composeFunctionis hoisted out ofcomputeFnbut the map lambda isn't, so the latter is rebuilt per output cell under vectorization. Neither captures anything; both belong at module level. - Line 248 — the lambda re-implements the
CellError/ExtendedNumberdispatch thatmapToRawScalarValue(line 65) already provides for the four*IF(S)siblings. Note the sequencing caveat there: if the boolean-handling fix lands, extending the helper is probably better than reusing it as-is.
These are separate from, and smaller than, the two points I raised as top-level comments (Excel boolean semantics, and the averageif/averageifs duplication).
Generated by Claude Code
| } | ||
|
|
||
| /** | ||
| * Corresponds to AVERAGEIFS(AverageRange, CriterionRange1, Criterion1, ...) |
There was a problem hiding this comment.
The JSDoc names the arguments differently from everything else this PR ships
Here: AverageRange, CriterionRange1, Criterion1 (and again on lines 222-223).
But statistical.ts:44 in this same PR publishes average_range, criteria_range1, criteria1, and those are what reach users — the generated guide page now renders:
AVERAGEIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)
and getFunctionDetails('AVERAGEIFS') returns the same three names. So there are two vocabularies for the same three arguments, and the one in the JSDoc doesn't exist anywhere a user can see it.
Worth aligning the JSDoc to the published names. It's the neighbouring convention too — the AVERAGEIF JSDoc above uses Range/Criterion/AverageRange, which at least match that function's own signature.
Generated by Claude Code
There was a problem hiding this comment.
Thanks for flagging. Fixed in commit c6e1fb5. The JSDoc now uses the published argument names average_range, criteria_range1, and criteria1.
| } | ||
| } | ||
|
|
||
| const computeFn = (values: SimpleRangeValue, ...args: unknown[]) => { |
There was a problem hiding this comment.
composeFunction is hoisted out of computeFn but the map lambda isn't — and neither needs to be here at all
composeFunction (line 231) is built once per averageifs() call. The map lambda (lines 248-256) is built inside computeFn, so it's rebuilt on every invocation of computeFn. Under array vectorization that's once per output cell — runFunction in FunctionPlugin.ts:427-431 loops the result array and calls calculateSingleCellOfResultArray (and hence computeFn) per cell.
Two pure functions sitting fifteen lines apart in the same method, one hoisted and one not, with nothing to justify the difference.
Neither captures anything from the enclosing scope — composeFunction closes over only CellError, and the map lambda over CellError, isExtendedNumber, getRawValue and AverageResult, all module-level imports. Neither touches this, functionName, values or args. So both can simply be module-level constants next to the existing zeroForInfinite (line 57) and mapToRawScalarValue (line 65), which is where this file already puts helpers of exactly this kind.
I won't oversell the performance side — the allocation is small next to the range scan it wraps. But DEV_DOCS does ask for attention here:
Consider the computational complexity of every change, especially in code that runs per cell, per formula, or per dependency-graph node.
and moving both out costs nothing and removes the inconsistency.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in commit c6e1fb5.
Both helpers are now module-level functions, and computeFn passes their references directly.
| functionName, | ||
| AverageResult.empty, | ||
| composeFunction, | ||
| (arg) => { |
There was a problem hiding this comment.
This re-implements the three-way split mapToRawScalarValue already does, 180 lines up
mapToRawScalarValue (line 65) is exactly this dispatch — CellError → itself, ExtendedNumber → raw number, anything else → undefined:
function mapToRawScalarValue(arg: InternalScalarValue): Maybe<CellError | RawScalarValue> {
if (arg instanceof CellError) { return arg }
if (isExtendedNumber(arg)) { return getRawValue(arg) }
return undefined
}It's the shared definition for SUMIF, SUMIFS, MINIFS and MAXIFS (lines 171, 186, 328, 355). This lambda spells the same three branches out again, so "what counts as an error" and "what counts as a number" now have two definitions in one file, and only the four *IF(S) siblings are discoverable to someone grepping for the helper.
It reuses cleanly with a typeof narrowing — no cast needed, unlike the existing call sites:
(arg) => {
const raw = mapToRawScalarValue(arg)
if (raw instanceof CellError) { return raw }
return typeof raw === 'number' ? AverageResult.single(raw) : AverageResult.empty
}One caveat, and it's why I'd sequence this after the boolean question. If AVERAGEIFS is changed to follow Excel on booleans (my separate comment above — TRUE → 1, FALSE → 0), then this mapper needs a branch mapToRawScalarValue doesn't have, since AVERAGEIF must keep ignoring booleans. At that point the better shape is probably to extend the helper — an optional "coerce booleans to numbers" parameter, defaulting to off — so there's still one definition of the dispatch, rather than a second inline copy that has drifted by one branch. Either way the goal is the same: don't leave two hand-written versions of this in the file.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in commit c6e1fb5cc.
mapToAverageResult now delegates scalar classification to mapToRawScalarValue, while preserving AVERAGEIFS error propagation and its verified behavior of ignoring Boolean cells.
Thanks for flagging. I verified this directly in Excel for the web. With |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1732 +/- ##
========================================
Coverage 97.32% 97.32%
========================================
Files 195 195
Lines 15739 15762 +23
Branches 3390 3467 +77
========================================
+ Hits 15318 15341 +23
+ Misses 421 413 -8
- Partials 0 8 +8
🚀 New features to boost your workflow:
|

Context
HyperFormula supports
AVERAGEIFand other conditional aggregation functions, but it does not supportAVERAGEIFS.This change adds
AVERAGEIFS, allowing values to be averaged when all corresponding criteria are satisfied. It uses the existing conditional aggregation and caching infrastructure and includes function metadata, translations, documentation, and tests.How did you test your changes?
Added tests covering:
All 28 focused tests passed.
Additionally, I ran:
npm run bundle:cjsnpm run docs:generate-function-docsBoth commands completed successfully.
Types of changes
expected anymore)
Related issues:
Checklist:
hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of
this project.
os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard.
CHANGELOG.md) file.
Note
Low Risk
New statistical function on established conditional-aggregation infrastructure; limited blast radius beyond formula evaluation behavior for
AVERAGEIFS/AVERAGEIFrefactors.Overview
Adds Excel-compatible
AVERAGEIFS, which averages numeric cells inaverage_rangeonly when every paired criteria range/criterion is satisfied.The implementation lives in
ConditionalAggregationPlugin, following the samerepeatLastArgspattern asSUMIFS/COUNTIFSand reusingcomputeConditionalAggregationFunctionwith shared helpers (composeAverageResults,mapToAverageResult,finalizeAverageResult) that also simplifyAVERAGEIFfinalization. Statistical function metadata and localized names are registered across the existing language packs, and the unreleased changelog documents the addition.Reviewed by Cursor Bugbot for commit c6e1fb5. Bugbot is set up for automated code reviews on this repo. Configure here.