Repository navigation
Add MUNIT, MDETERM and MINVERSE functions (HF-401, HF-399, HF-400) - #1817
marcin-kordas-hoc wants to merge 15 commits into
Conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | e6578aa | Commit Preview URL Branch Preview URL |
Nov 04 2026, 06:27 AM |
Performance comparison of head (a4f00d6) vs base (16c1caf) |
…it-mdeterm-minverse
…rtial pivoting The fraction-free elimination multiplied minors together, so a matrix with large or small elements overflowed or underflowed although its determinant was a number, and MINVERSE returned #NUM! for matrices that have an inverse. A singular matrix is now one whose elimination meets a pivot that is exactly 0, and the list of differences no longer claims an exact elimination. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and ruRU Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st MUNIT among such functions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion at parse time A dimension above the row or column limit of a sheet is now #VALUE!, like SEQUENCE, instead of allocating the matrix (a dimension of 20000 crashed the process). A dimension written as +3 or -3 is read when the result size is predicted, so =MUNIT(+3) spills a 3x3 matrix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n MDETERM and MINVERSE
Excel returns a 3x3 matrix for MUNIT("3") and #VALUE! for a matrix that holds both text and an
error, whichever comes first. MUNIT now takes its dimension as a number, and the result size of a
numeric text literal is known at parse time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Excel lifts MUNIT over a range and returns one result per cell; HyperFormula returns #VALUE!. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n does The predicted result size read a text literal with JavaScript number parsing, which ignores the configured decimal separator and accepts forms the engine rejects, so the prediction and the evaluated size could disagree. It now uses the same coercion as a NUMBER argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| public mdeterm(ast: ProcedureAst, state: InterpreterState): InterpreterValue { | ||
| return this.runFunction(ast.args, state, this.metadata('MDETERM'), (matrix: SimpleRangeValue) => { | ||
| const numbers = numbersOrError(matrix) | ||
| if (numbers instanceof CellError) { | ||
| return numbers | ||
| } | ||
| if (matrix.width() !== matrix.height()) { | ||
| return new CellError(ErrorType.VALUE, ErrorMessage.ArrayDimensions) | ||
| } | ||
| const result = determinant(numbers) | ||
| return Number.isFinite(result) ? result : new CellError(ErrorType.NUM, ErrorMessage.NaN) | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Corresponds to MINVERSE(Array). | ||
| * | ||
| * Returns the inverse of a square matrix of numbers. A non-square matrix or a non-numeric value returns | ||
| * the #VALUE! error. A matrix whose elimination meets a pivot that is exactly 0, or whose inverse has an element | ||
| * too large for a number, returns the #NUM! error. | ||
| */ | ||
| public minverse(ast: ProcedureAst, state: InterpreterState): InterpreterValue { | ||
| return this.runFunction(ast.args, state, this.metadata('MINVERSE'), (matrix: SimpleRangeValue) => { | ||
| const numbers = numbersOrError(matrix) | ||
| if (numbers instanceof CellError) { | ||
| return numbers | ||
| } | ||
| if (matrix.width() !== matrix.height()) { | ||
| return new CellError(ErrorType.VALUE, ErrorMessage.ArrayDimensions) | ||
| } |
There was a problem hiding this comment.
When the matrix is not square, MDETERM and MINVERSE return #VALUE! with "Array dimensions are not compatible.", which is MMULT's message for two arrays that can't be multiplied. These functions take a single argument, so that message doesn't tell the user what to fix.
const hf = HyperFormula.buildFromArray([
[1, 2, 3],
[4, 5, 6],
['=MDETERM(A1:C2)', '=MINVERSE(A1:C2)'],
], { licenseKey: 'gpl-v3' })
hf.getCellValue({ sheet: 0, row: 2, col: 0 }).message
// 'Array dimensions are not compatible.' (same for MINVERSE)Could we add a message in error-message.ts, next to SingularMatrix, and use it at lines 398 and 419?
public static SquareMatrix = 'Matrix must be square.'There was a problem hiding this comment.
Done in 443deef: MDETERM and MINVERSE return the new ErrorMessage.SquareMatrix ("Matrix must be square.") for a non-square matrix, so your 2x3 example shows it in both cells. The tests are in handsontable/hyperformula-tests@2e14cb3f5daf51467e030cbcf3ff626111cb5047.
| /** | ||
| * Reads a number written directly in a formula, with an optional sign, such as `3`, `+3` or `-3`. | ||
| * | ||
| * @param node - an argument of a function call | ||
| * @returns the number, or undefined when the argument is anything else and its value is known only after evaluation | ||
| */ | ||
| function numberLiteral(node: Ast): Maybe<number> { | ||
| if (node.type === AstNodeType.NUMBER) { | ||
| return node.value | ||
| } | ||
| if ((node.type === AstNodeType.PLUS_UNARY_OP || node.type === AstNodeType.MINUS_UNARY_OP) && node.value.type === AstNodeType.NUMBER) { | ||
| return node.type === AstNodeType.MINUS_UNARY_OP ? -node.value.value : node.value.value | ||
| } | ||
| return undefined | ||
| } |
There was a problem hiding this comment.
Nit: numberLiteral duplicates the signed-literal handling in SequencePlugin.parseLiteralDimension. Both read a parse-time dimension from a NUMBER node, or a PLUS_UNARY_OP/MINUS_UNARY_OP wrapped around one, using separate code. The two copies have already drifted: SEQUENCE also accepts TRUE()/FALSE() and reads text with Number(), while MUNIT coerces text the way evaluation does. A future fix to one copy could easily miss the other.
Could we move a single literal-dimension reader into FunctionPlugin, next to coerceScalarToNumberOrError, and call it from both size methods? Bringing SEQUENCE's text handling in line can be a separate follow-up.
There was a problem hiding this comment.
Done in f638d2c: FunctionPlugin.signedNumberLiteral reads a NUMBER node, bare or under a unary + or -, and both munitArraySize and SequencePlugin.parseLiteralDimension call it. Text handling stays per caller and SEQUENCE keeps TRUE()/FALSE(), for the follow-up you suggested.
…it-mdeterm-minverse
…zes in one FunctionPlugin method
# Conflicts: # CHANGELOG.md # hyperformula/src/license/functionCapabilities.ts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a4f00d6. Configure here.
| } | ||
| if (matrix.width() !== matrix.height()) { | ||
| return new CellError(ErrorType.VALUE, ErrorMessage.SquareMatrix) | ||
| } |
There was a problem hiding this comment.
Non-square error matrix skips #VALUE!
Medium Severity
When a non-square matrix also holds a cell error, MDETERM and MINVERSE propagate that error instead of #VALUE!. numbersOrError returns the first inner error before the square-shape check, so ErrorMessage.SquareMatrix never applies in that case.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a4f00d6. Configure here.
# Conflicts: # CHANGELOG.md
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1817 +/- ##
===========================================
+ Coverage 97.23% 97.38% +0.15%
===========================================
Files 209 209
Lines 16524 16642 +118
Branches 3636 3666 +30
===========================================
+ Hits 16067 16207 +140
+ Misses 445 427 -18
+ Partials 12 8 -4
🚀 New features to boost your workflow:
|


Context
Adds the matrix functions
MUNIT,MDETERMandMINVERSEtoMatrixPlugin, next toMMULT.MUNIT(dimension)returns the identity matrix of that size. Numeric text such as"3"is read as a number, with the configured decimal separator, and a fractional dimension is truncated. A dimension below 1, text, an empty text, an empty cell or a dimension above the sheet's row or column limit is#VALUE!, andTRUE()is 1.MDETERM(array)returns the determinant of a square matrix of numbers. A matrix that is not square, or that holds text, an empty cell or a boolean, is#VALUE!, even when it also holds an error. A non-square matrix of numbers gets the message "Matrix must be square." Otherwise an error inside the matrix propagates. A single number is its own determinant.MINVERSE(array)returns the inverse of a square matrix, with the same#VALUE!cases. A singular matrix is#NUM!.MDETERMmultiplies the pivots, andMINVERSEsolves one pair of triangular systems per column. A matrix is singular when the elimination meets a pivot that is exactly 0.#NUM!instead of its determinant (about 6E300), andMINVERSEreturned#NUM!for matrices that have an inverse.MDETERM({1,2,3;4,5,6;7,8,9})returns about 6.66E-16 in both, andMINVERSEof that matrix returns about -4.5E+15 at position (1,1) in both, instead of#NUM!.MUNIT, esES isM.UNIDADand ruRU isМЕДИН, taken from those pages. idID keeps the English names.MUNITandSEQUENCEread a dimension written as a number, with an optional sign, through oneFunctionPluginmethod,signedNumberLiteral. Each keeps its own handling of a text dimension, andSEQUENCEkeepsTRUE()andFALSE(); noSEQUENCEresult changes.MUNITadded to the known limitation on result sizes that must be known at parse time.How did you test your changes?
feat/hf-matrix-munit-mdeterm-minverseinhyperformula-tests. It addsunit/interpreter/function-munit.spec.ts,function-mdeterm.spec.tsandfunction-minverse.spec.ts, with 51, 48 and 58 tests of one assertion each, andmatrix-functions-layout.ts, the sheet the expected values were measured on.MUNITorMINVERSEforced to one cell, 6 and 3 tests fail, the#SPILL!tests among them. WithsignedNumberLiteralignoring a unary plus, the=MUNIT(+2)spill test and 2SEQUENCEtests fail. All pass now.#NUM!or 0 for 11 of the 108. LU returns#NUM!for 1: its pivots overflow when multiplied, although the determinant is about 3E137. Excel also returns#NUM!for a matrix whose pivot product overflows (table below). The previous elimination also returned#NUM!fromMINVERSEfor 3 invertible matrices; LU for none.INDEX(MINVERSE(...))of{1,2,3;4,5,6;7,8,9}scaled by a power of ten. That matrix is singular, and it now gives#NUM!instead of rounding noise.jestrun on this head with the paired tests: 523 suites, 6899 passed, 3 skipped.Compatibility with Excel and Google Sheets
60 formulas were evaluated in Excel Online on one data layout (A1:B2 =
{1,2;3,4}, A3 = text); 58 match. Selected rows:=INDEX(MUNIT(3),2,2)11=ROWS(MUNIT(2.9))22=MUNIT(0)#VALUE!#VALUE!=MUNIT(TRUE())11=MDETERM(A1:B2)-2-2=MDETERM(A1:C2)#VALUE!#VALUE!=MDETERM(TRUE())#VALUE!#VALUE!=INDEX(MINVERSE(A1:B2),2,1)1.51.5=MINVERSE(0)#NUM!#NUM!=MDETERM(A1:B2*2)-8#VALUE!;-8withuseArrayArithmetic=MINVERSE(A3:B4)(a range with text)#VALUE!#VALUE!in each cell of the predicted 2x2 result,#SPILL!when that area is occupiedThe last two rows are differences.
MDETERM(A1:B2*2)needs array arithmetic, which HyperFormula enables only withuseArrayArithmetic.MINVERSEsizes its result before it evaluates the argument, so an error result still occupies a 2x2 area.14 more formulas were measured in Excel Online after the first round; 13 match:
=MDETERM({1,2,3;4,5,6;7,8,9})6.66E-166.66E-16=INDEX(MINVERSE({1,2,3;4,5,6;7,8,9}),1,1)-4.5036E+15-4.5036E+15=ROWS(MUNIT("3"))33=ROWS(MUNIT(+3))33=MDETERM(A1:C3)with{1,2,0;0,3,1;2,1,1}times 1E1006E+3006E+300=MDETERM(A1:C3)with the same matrix times 1E-906E-2706E-270=MDETERM(A1:B2)with{1E200,0;0,1E200}#NUM!#NUM!=MDETERM(A1:C3)with{1E200,0,0;0,1E200,0;0,0,1E-200}#NUM!#NUM!=INDEX(MINVERSE(A1:C3),1,1)with{1E-170,0,0;0,1,0;0,0,1E-170}1E+1701E+170=INDEX(MINVERSE(A1),1,1)with A1 = 1E-3071E+3071E+307=INDEX(MINVERSE(1e-307/1000),1,1)#NUM!#NUM!=MDETERM(A1:B2)with{1,"x";=1/0,4}#VALUE!#VALUE!=MDETERM(A1:B2)with{TRUE,2;3,4}#VALUE!#VALUE!=ROWS(MUNIT(A1:A2))with A1:A2 = 3, 22#VALUE!The last row is a difference: Excel returns one result per cell of a range of dimensions, and HyperFormula does not. It is listed in
docs/guide/list-of-differences.md.6 more formulas were measured in Excel Online on 2026-11-02; all match. Each
#SPILL!row has a value inside the area the result needs, and the same formula on an empty area spills in both.=MUNIT(2)in A1, B2 occupied#SPILL!#SPILL!=MINVERSE(A1:B2)in C1, D2 occupied#SPILL!#SPILL!=MINVERSE({1,2;3,4})in A1, B2 occupied#SPILL!#SPILL!=MDETERM(A1:C2)with A1:C2 ={1,2,3;4,5,6}#VALUE!#VALUE!=MINVERSE(A1:C2)with the same range#VALUE!#VALUE!=SUMPRODUCT(ABS(MMULT(D1:F3,MINVERSE(D1:F3))-{1,0,0;0,1,0;0,0,1})), D1:F3 ={2,1,1;1,3,2;1,0,0}3.1E-152.7E-15withuseArrayArithmeticPublic API changes
None.
Out of scope and follow-ups
getRegisteredFunctionNames. This already affects every ruRU function and is not changed here.MUNITwith a cell reference or a formula as the dimension (=MUNIT(A1)) is#VALUE!unless the result is 1x1, likeSEQUENCE; see the known limitations.SEQUENCEstill reads a text dimension at parse time with JavaScriptNumber(), whileMUNITcoerces it the way evaluation does. AligningSEQUENCEis a follow-up.Types of changes
Related issues
Checklist
dev-docs/pages this change affects – or none needed it.🤖 Generated with Claude Code
Note
Low Risk
Additive formula features in MatrixPlugin with no public API changes; numerical edge cases follow Excel-style LU behavior already described in tests.
Overview
Adds Excel-style matrix functions
MUNIT,MDETERM, andMINVERSEinMatrixPlugin, alongside existingMMULT.MUNIT(dimension)builds an identity matrix (dimension truncated, capped by sheet limits).MDETERMandMINVERSErequire square numeric arrays; non-square inputs use newSquareMatrixmessages, singular inverses useSingularMatrix/#NUM!. Both share LU decomposition with partial pivoting—determinant from pivot products, inverse via triangular solves.Parse-time spill sizing:
MUNITandMINVERSEdeclare result dimensions before evaluation.signedNumberLiteralonFunctionPlugincentralizes reading literal dimensions (including unary+/-);SequencePluginnow uses it forSEQUENCEwithout intended behavior change.Also updates localized names across language packs, license capabilities, function catalog metadata, CHANGELOG, docs (
MUNITin parse-time array-size limits; Excel difference forMUNITwith a range argument).Reviewed by Cursor Bugbot for commit e6578aa. Bugbot is set up for automated code reviews on this repo. Configure here.