Skip to content

Fix MAXPOOL and MEDIANPOOL throwing on non-tiling dimensions - #1718

Draft
sequba wants to merge 2 commits into
developfrom
claude/vigilant-gates-3dkoua
Draft

Fix MAXPOOL and MEDIANPOOL throwing on non-tiling dimensions#1718
sequba wants to merge 2 commits into
developfrom
claude/vigilant-gates-3dkoua

Conversation

@sequba

@sequba sequba commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Context

MAXPOOL and MEDIANPOOL computed the output array size without checking that the pooling window actually tiles the input range. When the range dimensions, reduced by the window size, were not whole multiples of the stride, the kernel read past the last row of the input, and an uncaught TypeError escaped the interpreter and the public API:

const hf = HyperFormula.buildEmpty({licenseKey: 'gpl-v3'})
const id = hf.getSheetId(hf.addSheet('S'))
hf.setSheetContent(id, [[3, 1, 2], [9, 7, 8], [5, 4, 6]])   // 3x3

hf.calculateFormula('=MAXPOOL(A1:C3, 2)', id)
// TypeError: Cannot read properties of undefined (reading '0')
hf.calculateFormula('=MEDIANPOOL(A1:C3, 2)', id)            // same

The same exception escaped setCellContents(). maxpoolArraySize() already rejected such arguments, but ArraySize.error() is (1, 1, isRef: true), which isScalar() reports as scalar, so the formula was evaluated as an ordinary scalar formula and the invalid arguments reached the kernel anyway.

Related failure modes found while reproducing: a window larger than the range, a stride that makes the last window overshoot, and a zero, negative or fractional window size or stride all threw as well (TypeError or RangeError: Invalid array length).

Changes:

  • MatrixPlugin.maxpool() / MatrixPlugin.medianpool() now validate the window size and the stride against the range dimensions and return #VALUE! with a new ErrorMessage.PoolDimensions ('Range dimensions are not compatible with the window size and the stride.') instead of running the kernel out of bounds. #VALUE! matches how MMULT reports incompatible dimensions.
  • The window size and the stride are now declared as FunctionArgumentType.INTEGER with minValue: 1, so a zero, negative or fractional value returns #NUM! (Value too small. / Value needs to be an integer.) instead of throwing.
  • The new isPoolWindowFittingInputArray() predicate is shared with maxpoolArraySize(), so the predicted array size and the runtime validation stay in sync.
  • The constraint is documented in the built-in functions guide and in the JSDoc of both methods. The catalogue entries mentioned in the report (src/interpreter/functionMetadata/categories/matrix-functions.ts) do not exist on develop — the parameter metadata lives in MatrixPlugin.implementedFunctions — so the prose went to docs/guide/built-in-functions.md instead.

How did you test your changes?

Added test/unit/interpreter/matrix-plugin-pooling.spec.ts (20 tests). The error cases are shared between MAXPOOL and MEDIANPOOL via a forEach loop over the function names — deliberately not describe.each, which exists only in Jest and breaks the Jasmine-based browser suite (see the second commit below). Coverage:

  • dimensions not a whole multiple of the window size, via both calculateFormula() and setCellContents();
  • only one of the two dimensions divisible (3x4 range, window 2);
  • window larger than the range;
  • stride that makes the last window reach outside the range;
  • zero, negative and fractional window size and stride;
  • regression guards for the working cases: 4x4 with window 2, 3x3 with window 2 and stride 1, window covering the whole range, and spilling the pooled values into cells.

Verified that the 14 validation tests fail with the original TypeError / RangeError when the MatrixPlugin change is reverted, and that the 6 regression guards pass both before and after the fix.

Ran locally in both test runners, since the spec is picked up by each: npx jest (24 passed) and Karma/Jasmine under Chromium (Executed 24 of 24 SUCCESS). npm run lint (0 errors) and npm run compile also pass.

Commits

  1. Fix MAXPOOL and MEDIANPOOL throwing on non-tiling dimensions — the fix, tests, docs and changelog entry.
  2. Replace describe.each with a loop in the pooling testskarma.starter.ts loads every test/**/*.spec.ts under Jasmine, which has no describe.each, so the first commit made the whole Karma run fail with TypeError: describe.each is not a function (Executed 6080 of 6083 ... ERROR in both Chrome and Firefox). A plain forEach works in both runners.

Types of changes

  • Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore)
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues:

  1. Fixes #...

Checklist:

  • I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project.
  • I have signed the Contributor License Agreement.
  • My change is compliant with the OpenDocument standard. — MAXPOOL and MEDIANPOOL are HyperFormula-specific functions, not covered by the standard.
  • My change is compatible with Microsoft Excel. — not applicable, these functions do not exist in Excel.
  • My change is compatible with Google Sheets. — not applicable, these functions do not exist in Google Sheets.
  • I described my changes in the CHANGELOG.md file.
  • My changes require a documentation update.
  • My changes require a migration guide.

MAXPOOL and MEDIANPOOL computed the output array size without checking
that the pooling window actually tiles the input range. When the range
dimensions, reduced by the window size, were not whole multiples of the
stride, the kernel read past the last row of the input and an uncaught
TypeError escaped the interpreter and the public API.

- Validate the window size and the stride against the range dimensions
  and return #VALUE! (ErrorMessage.PoolDimensions) instead.
- Require the window size and the stride to be positive integers, so that
  a zero, negative or fractional value returns #NUM! instead of throwing.
- Reuse the new predicate in maxpoolArraySize to keep the predicted array
  size and the runtime validation in sync.
- Document the constraint in the built-in functions guide and in the
  method JSDoc.
@netlify

netlify Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploy Preview for hyperformula-dev-docs ready!

Name Link
🔨 Latest commit 725b84d
🔍 Latest deploy log https://app.netlify.com/projects/hyperformula-dev-docs/deploys/6a6b30d26bd21e0008bb905f
😎 Deploy Preview https://deploy-preview-1718--hyperformula-dev-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

The browser test suite runs under Karma and Jasmine, which has no
describe.each, so the Jest-only helper made the whole Karma run fail with
"TypeError: describe.each is not a function". A plain forEach over the
function names works in both runners.
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Performance comparison of head (725b84d) vs base (ad142ba)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |  508.36 |  505.49 | -0.56%
                                      Sheet B |  168.09 |  164.56 | -2.10%
                                      Sheet T |   147.1 |  143.98 | -2.12%
                                Column ranges |  537.47 |  532.37 | -0.95%
                                Sorted lookup | 16004.2 | 15598.1 | -2.54%
Sheet A:  change value, add/remove row/column |   19.76 |   18.22 | -7.79%
 Sheet B: change value, add/remove row/column |  160.64 |  171.01 | +6.46%
                   Column ranges - add column |  182.83 |  180.63 | -1.20%
                Column ranges - without batch |   538.9 |   535.3 | -0.67%
                        Column ranges - batch |  131.94 |  133.13 | +0.90%

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.22%. Comparing base (ad142ba) to head (725b84d).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop    #1718   +/-   ##
========================================
  Coverage    97.22%   97.22%           
========================================
  Files          178      178           
  Lines        15612    15624   +12     
  Branches      3430     3434    +4     
========================================
+ Hits         15178    15191   +13     
+ Misses         426      425    -1     
  Partials         8        8           
Files with missing lines Coverage Δ
src/error-message.ts 100.00% <100.00%> (ø)
src/interpreter/plugin/MatrixPlugin.ts 98.69% <100.00%> (+0.80%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants