diff --git a/CHANGELOG.md b/CHANGELOG.md index 323c746b2d..2c01f5e2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added a message after `npm i hyperformula` that points AI assistants to the SKILLS file. The message needs `--foreground-scripts` to be visible in npm, and pnpm 10 needs the `postinstall` script to be approved first. [#1829](https://github.com/handsontable/hyperformula/pull/1829) +- Added the `MUNIT`, `MDETERM` and `MINVERSE` matrix functions: the identity matrix, the determinant and the inverse of a square matrix. [#1817](https://github.com/handsontable/hyperformula/pull/1817) ### Fixed diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index d78cac018d..c57b98d411 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -45,7 +45,7 @@ a circular reference. * For certain inputs, the RATE function might have no solutions, or have multiple solutions. Our implementation uses an iterative algorithm (Newton's method) to find an approximation for one of the solutions to within 1e-7. If the approximation is not found after 50 iterations, the RATE function returns the `#NUM!` error. * The INDEX function doesn't support returning whole rows or columns of the source range – it always returns the contents of a single cell. * The FILTER function accepts either single rows of equal width or single columns of equal height. In other words, all arrays passed to the FILTER function must have equal dimensions, and at least one of those dimensions must be 1. -* Array-producing functions (e.g., SEQUENCE, FILTER) require their output dimensions to be determinable at parse time. Passing cell references or formulas as dimension arguments (e.g., `=SEQUENCE(A1)`) results in a `#VALUE!` error, because the output size cannot be resolved before evaluation. +* Array-producing functions (e.g., SEQUENCE, MUNIT, FILTER) require their output dimensions to be determinable at parse time. Passing cell references or formulas as dimension arguments (e.g., `=SEQUENCE(A1)`) results in a `#VALUE!` error, because the output size cannot be resolved before evaluation. * The TEXT function does not accept embedded double-quote literals in the format string. In Excel, `""` inside a format string is an escape sequence for a literal `"` character — e.g. `=TEXT(1234.5, "#,##0.00 ""zł""")` returns `"1,234.50 zł"`. If your application requires this escape sequence, supply a custom [`stringifyCurrency`](currency-handling.md) callback. ### UNIQUE function diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index 660fda0f98..f5ffbb50bb 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -122,6 +122,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | ISODD | =ISODD(3.5) | FALSE | TRUE | TRUE | | CEILING.MATH | =CEILING.MATH(-4.3, 2, 2) | -4 | -6 | -6 | | FLOOR.MATH | =FLOOR.MATH(-4.7, 2, 2) | -6 | -4 | -4 | +| MUNIT | =ROWS(MUNIT(A1:A2))
where A1:A2: 3, 2 | VALUE | Not checked | 2 | A few of the rows above share a root cause worth stating once: diff --git a/hyperformula/src/error-message.ts b/hyperformula/src/error-message.ts index d42040901c..749e4c2f05 100644 --- a/hyperformula/src/error-message.ts +++ b/hyperformula/src/error-message.ts @@ -13,6 +13,7 @@ export class ErrorMessage { public static EmptyArray = 'Empty array not allowed.' public static ArrayDimensions = 'Array dimensions are not compatible.' public static PoolDimensions = 'Range dimensions are not compatible with the window size and the stride.' + public static SingularMatrix = 'Matrix is singular.' public static NoSpaceForArrayResult = 'No space for array result.' public static ValueSmall = 'Value too small.' public static ValueLarge = 'Value too large.' diff --git a/hyperformula/src/i18n/languages/csCZ.ts b/hyperformula/src/i18n/languages/csCZ.ts index fea286083c..62eba54e28 100644 --- a/hyperformula/src/i18n/languages/csCZ.ts +++ b/hyperformula/src/i18n/languages/csCZ.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MINIFS', MINUTE: 'MINUTA', MIRR: 'MOD.MÍRA.VÝNOSNOSTI', + MDETERM: 'DETERMINANT', + MINVERSE: 'INVERZE', MMULT: 'SOUČIN.MATIC', + MUNIT: 'MUNIT', MOD: 'MOD', MONTH: 'MĚSÍC', N: 'N', diff --git a/hyperformula/src/i18n/languages/daDK.ts b/hyperformula/src/i18n/languages/daDK.ts index 9264551249..e53a38e182 100644 --- a/hyperformula/src/i18n/languages/daDK.ts +++ b/hyperformula/src/i18n/languages/daDK.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MINHVISER', MINUTE: 'MINUT', MIRR: 'MIA', + MDETERM: 'MDETERM', + MINVERSE: 'MINVERT', MMULT: 'MPRODUKT', + MUNIT: 'MENHED', MOD: 'REST', MONTH: 'MÅNED', N: 'TAL', diff --git a/hyperformula/src/i18n/languages/deDE.ts b/hyperformula/src/i18n/languages/deDE.ts index 4d3d39319b..7c7156f471 100644 --- a/hyperformula/src/i18n/languages/deDE.ts +++ b/hyperformula/src/i18n/languages/deDE.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MINWENNS', MINUTE: 'MINUTE', MIRR: 'QIKV', + MDETERM: 'MDET', + MINVERSE: 'MINV', MMULT: 'MMULT', + MUNIT: 'MEINHEIT', MOD: 'REST', MONTH: 'MONAT', N: 'N', diff --git a/hyperformula/src/i18n/languages/enGB.ts b/hyperformula/src/i18n/languages/enGB.ts index d271b732cd..8e1d290288 100644 --- a/hyperformula/src/i18n/languages/enGB.ts +++ b/hyperformula/src/i18n/languages/enGB.ts @@ -161,7 +161,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MINIFS', MINUTE: 'MINUTE', MIRR: 'MIRR', + MDETERM: 'MDETERM', + MINVERSE: 'MINVERSE', MMULT: 'MMULT', + MUNIT: 'MUNIT', MOD: 'MOD', MONTH: 'MONTH', N: 'N', diff --git a/hyperformula/src/i18n/languages/esES.ts b/hyperformula/src/i18n/languages/esES.ts index 9c6b817b98..81c2fe309b 100644 --- a/hyperformula/src/i18n/languages/esES.ts +++ b/hyperformula/src/i18n/languages/esES.ts @@ -160,7 +160,10 @@ export const dictionary: RawTranslationPackage = { MINIFS: 'MIN.SI.CONJUNTO', MINUTE: 'MINUTO', MIRR: 'TIRM', + MDETERM: 'MDETERM', + MINVERSE: 'MINVERSA', MMULT: 'MMULT', + MUNIT: 'M.UNIDAD', MOD: 'RESIDUO', MONTH: 'MES', N: 'N', diff --git a/hyperformula/src/i18n/languages/fiFI.ts b/hyperformula/src/i18n/languages/fiFI.ts index 99ff05708f..36a1a79f56 100644 --- a/hyperformula/src/i18n/languages/fiFI.ts +++ b/hyperformula/src/i18n/languages/fiFI.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.JOS', MINUTE: 'MINUUTIT', MIRR: 'MSISÄINEN', + MDETERM: 'MDETERM', + MINVERSE: 'MKÄÄNTEINEN', MMULT: 'MKERRO', + MUNIT: 'YKSIKKÖM', MOD: 'JAKOJ', MONTH: 'KUUKAUSI', N: 'N', diff --git a/hyperformula/src/i18n/languages/frFR.ts b/hyperformula/src/i18n/languages/frFR.ts index f7b9be45f7..46a129b80f 100644 --- a/hyperformula/src/i18n/languages/frFR.ts +++ b/hyperformula/src/i18n/languages/frFR.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.SI', MINUTE: 'MINUTE', MIRR: 'TRIM', + MDETERM: 'DETERMAT', + MINVERSE: 'INVERSEMAT', MMULT: 'PRODUITMAT', + MUNIT: 'MATRICE.UNITAIRE', MOD: 'MOD', MONTH: 'MOIS', N: 'N', diff --git a/hyperformula/src/i18n/languages/huHU.ts b/hyperformula/src/i18n/languages/huHU.ts index e40219553a..c6814cbcca 100644 --- a/hyperformula/src/i18n/languages/huHU.ts +++ b/hyperformula/src/i18n/languages/huHU.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MINHA', MINUTE: 'PERCEK', MIRR: 'MEGTÉRÜLÉS', + MDETERM: 'MDETERM', + MINVERSE: 'INVERZ.MÁTRIX', MMULT: 'MSZORZAT', + MUNIT: 'MMÁTRIX', MOD: 'MARADÉK', MONTH: 'HÓNAP', N: 'S', diff --git a/hyperformula/src/i18n/languages/idID.ts b/hyperformula/src/i18n/languages/idID.ts index 719cb11db6..f60293e89b 100644 --- a/hyperformula/src/i18n/languages/idID.ts +++ b/hyperformula/src/i18n/languages/idID.ts @@ -161,7 +161,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.JIKAS', MINUTE: 'MENIT', MIRR: 'MIRR', + MDETERM: 'MDETERM', + MINVERSE: 'MINVERSE', MMULT: 'MMULT', + MUNIT: 'MUNIT', MOD: 'MOD', MONTH: 'BULAN', N: 'N', diff --git a/hyperformula/src/i18n/languages/itIT.ts b/hyperformula/src/i18n/languages/itIT.ts index 1a494628f8..5e54513bc1 100644 --- a/hyperformula/src/i18n/languages/itIT.ts +++ b/hyperformula/src/i18n/languages/itIT.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.PIÙ.SE', MINUTE: 'MINUTO', MIRR: 'TIR.VAR', + MDETERM: 'MATR.DETERM', + MINVERSE: 'MATR.INVERSA', MMULT: 'MATR.PRODOTTO', + MUNIT: 'MATR.UNIT', MOD: 'RESTO', MONTH: 'MESE', N: 'NUM', diff --git a/hyperformula/src/i18n/languages/nbNO.ts b/hyperformula/src/i18n/languages/nbNO.ts index ff15415a98..483d8d0e17 100644 --- a/hyperformula/src/i18n/languages/nbNO.ts +++ b/hyperformula/src/i18n/languages/nbNO.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.HVIS.SETT', MINUTE: 'MINUTT', MIRR: 'MODIR', + MDETERM: 'MDETERM', + MINVERSE: 'MINVERS', MMULT: 'MMULT', + MUNIT: 'MENHET', MOD: 'REST', MONTH: 'MÅNED', N: 'N', diff --git a/hyperformula/src/i18n/languages/nlNL.ts b/hyperformula/src/i18n/languages/nlNL.ts index c491e2f44b..a74768736f 100644 --- a/hyperformula/src/i18n/languages/nlNL.ts +++ b/hyperformula/src/i18n/languages/nlNL.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.ALS.VOORWAARDEN', MINUTE: 'MINUUT', MIRR: 'GIR', + MDETERM: 'DETERMINANTMAT', + MINVERSE: 'INVERSEMAT', MMULT: 'PRODUCTMAT', + MUNIT: 'EENHEIDMAT', MOD: 'REST', MONTH: 'MAAND', N: 'N', diff --git a/hyperformula/src/i18n/languages/plPL.ts b/hyperformula/src/i18n/languages/plPL.ts index 6fa4f016b4..4eac2fd86b 100644 --- a/hyperformula/src/i18n/languages/plPL.ts +++ b/hyperformula/src/i18n/languages/plPL.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MIN.WARUNKÓW', MINUTE: 'MINUTA', MIRR: 'MIRR', + MDETERM: 'WYZNACZNIK.MACIERZY', + MINVERSE: 'MACIERZ.ODW', MMULT: 'MACIERZ.ILOCZYN', + MUNIT: 'MACIERZ.JEDNOSTKOWA', MOD: 'MOD', MONTH: 'MIESIĄC', N: 'N', diff --git a/hyperformula/src/i18n/languages/ptPT.ts b/hyperformula/src/i18n/languages/ptPT.ts index 6c1e68c328..cf1e979641 100644 --- a/hyperformula/src/i18n/languages/ptPT.ts +++ b/hyperformula/src/i18n/languages/ptPT.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MÍNIMOSES', MINUTE: 'MINUTO', MIRR: 'MTIR', + MDETERM: 'MATRIZ.DETERM', + MINVERSE: 'MATRIZ.INVERSA', MMULT: 'MATRIZ.MULT', + MUNIT: 'UNIDM', MOD: 'MOD', MONTH: 'MÊS', N: 'N', diff --git a/hyperformula/src/i18n/languages/ruRU.ts b/hyperformula/src/i18n/languages/ruRU.ts index 6b35c1ce2f..f4307265d2 100644 --- a/hyperformula/src/i18n/languages/ruRU.ts +++ b/hyperformula/src/i18n/languages/ruRU.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'МИНЕСЛИ', MINUTE: 'МИНУТЫ', MIRR: 'МВСД', + MDETERM: 'МОПРЕД', + MINVERSE: 'МОБР', MMULT: 'МУМНОЖ', + MUNIT: 'МЕДИН', MOD: 'ОСТАТ', MONTH: 'МЕСЯЦ', N: 'Ч', diff --git a/hyperformula/src/i18n/languages/svSE.ts b/hyperformula/src/i18n/languages/svSE.ts index 5f87580087..1b04693b78 100644 --- a/hyperformula/src/i18n/languages/svSE.ts +++ b/hyperformula/src/i18n/languages/svSE.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'MINIFS', MINUTE: 'MINUT', MIRR: 'MODIR', + MDETERM: 'MDETERM', + MINVERSE: 'MINVERT', MMULT: 'MMULT', + MUNIT: 'MENHET', MOD: 'REST', MONTH: 'MÅNAD', N: 'N', diff --git a/hyperformula/src/i18n/languages/trTR.ts b/hyperformula/src/i18n/languages/trTR.ts index 93c2176d37..d507c2ee63 100644 --- a/hyperformula/src/i18n/languages/trTR.ts +++ b/hyperformula/src/i18n/languages/trTR.ts @@ -160,7 +160,10 @@ const dictionary: RawTranslationPackage = { MINIFS: 'ÇOKEĞERMİN', MINUTE: 'DAKİKA', MIRR: 'D_İÇ_VERİM_ORANI', + MDETERM: 'DETERMİNANT', + MINVERSE: 'DİZEY_TERS', MMULT: 'DÇARP', + MUNIT: 'BİRİMMATRİS', MOD: 'MOD', MONTH: 'AY', N: 'S', diff --git a/hyperformula/src/interpreter/functionMetadata/categories/matrix-functions.ts b/hyperformula/src/interpreter/functionMetadata/categories/matrix-functions.ts index 841285db41..ef6d4c68f0 100644 --- a/hyperformula/src/interpreter/functionMetadata/categories/matrix-functions.ts +++ b/hyperformula/src/interpreter/functionMetadata/categories/matrix-functions.ts @@ -24,6 +24,20 @@ export const MATRIX_FUNCTIONS_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=MEDIANPOOL(A1:D4, 2)', '=MEDIANPOOL(A1:D4, 2, 1)'], }, + MDETERM: { + category: 'Matrix functions', + shortDescription: 'Calculates the determinant of a square array. A non-square array or an array with a non-numeric value returns the #VALUE! error.', + parameters: [{name: 'array', description: 'The square range or array of numbers whose determinant is calculated.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=MDETERM(A1:B2)', '=MDETERM({1,2;3,4})'], + }, + MINVERSE: { + category: 'Matrix functions', + shortDescription: 'Calculates the inverse of a square array. A non-square array or an array with a non-numeric value returns the #VALUE! error; a singular array returns the #NUM! error.', + parameters: [{name: 'array', description: 'The square range or array of numbers to invert.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=MINVERSE(A1:B2)', '=MINVERSE({1,2;3,4})'], + }, MMULT: { category: 'Matrix functions', shortDescription: 'Calculates the array product of two arrays.', @@ -31,4 +45,11 @@ export const MATRIX_FUNCTIONS_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=MMULT(A1:B2, D1:E2)', '=MMULT(A1:C2, A4:B6)'], }, + MUNIT: { + category: 'Matrix functions', + shortDescription: 'Returns the identity matrix of the given dimension. A fractional dimension is truncated; a dimension below 1 or a non-numeric one returns the #VALUE! error.', + parameters: [{name: 'dimension', description: 'The number of rows and columns of the returned matrix.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=MUNIT(3)', '=MMULT(A1:C3, MUNIT(3))'], + }, } diff --git a/hyperformula/src/interpreter/plugin/MatrixPlugin.ts b/hyperformula/src/interpreter/plugin/MatrixPlugin.ts index e6235f5d89..545c114ccd 100644 --- a/hyperformula/src/interpreter/plugin/MatrixPlugin.ts +++ b/hyperformula/src/interpreter/plugin/MatrixPlugin.ts @@ -6,9 +6,9 @@ import {ArraySize} from '../../ArraySize' import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' -import {AstNodeType, ProcedureAst} from '../../parser' +import {Ast, AstNodeType, ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' -import {InternalScalarValue, InterpreterValue} from '../InterpreterValue' +import {getRawValue, InternalScalarValue, InterpreterValue, isExtendedNumber} from '../InterpreterValue' import {Maybe} from '../../Maybe' import {SimpleRangeValue} from '../../SimpleRangeValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' @@ -79,6 +79,161 @@ function poolFunctionArgumentsError(matrix: SimpleRangeValue, windowSize: number return undefined } +/** + * Reads the numbers of a matrix argument, treating typed numbers (percentages, dates, currency) as plain numbers. + * + * @param matrix - the matrix argument + * @returns the numbers of the matrix; the #VALUE! error when it holds any value that is neither a number nor an error, even + * after an error; otherwise the first error found in it + */ +function numbersOrError(matrix: SimpleRangeValue): number[][] | CellError { + let hasNonNumericValue = false + let firstError: Maybe + const numbers: number[][] = [] + + for (const row of matrix.rawData()) { + const numbersInRow: number[] = [] + for (const value of row) { + if (value instanceof CellError) { + firstError = firstError ?? value + } else if (isExtendedNumber(value)) { + numbersInRow.push(getRawValue(value)) + } else { + hasNonNumericValue = true + } + } + numbers.push(numbersInRow) + } + + if (hasNonNumericValue) { + return new CellError(ErrorType.VALUE, ErrorMessage.NumberRange) + } + return firstError ?? numbers +} + +/** + * The LU decomposition of a square matrix with partial pivoting: the rows of the matrix, reordered by `permutation`, + * equal the product of a lower triangular matrix with ones on the diagonal and an upper triangular matrix. + */ +interface LuDecomposition { + /** The upper triangular factor on and above the diagonal, the multipliers of the lower one below it. */ + factors: number[][], + /** For each row of `factors`, the index of the row of the original matrix it comes from. */ + permutation: number[], + /** 1 or -1: the sign of the permutation, by which the product of the pivots is multiplied to give the determinant. */ + sign: number, +} + +/** + * Factorizes a square matrix with Gaussian elimination and partial pivoting: at each step, the row with the entry of + * the greatest magnitude in the pivot column becomes the pivot row. + * + * @param matrix - a square matrix, left unchanged + * @returns the decomposition, or undefined when a pivot is exactly 0, that is when the matrix is singular + */ +function luDecomposition(matrix: number[][]): Maybe { + const size = matrix.length + const factors = matrix.map((row) => row.slice()) + const permutation = factors.map((_, index) => index) + let sign = 1 + + for (let k = 0; k < size; k++) { + let pivotRow = k + for (let i = k + 1; i < size; i++) { + if (Math.abs(factors[i][k]) > Math.abs(factors[pivotRow][k])) { + pivotRow = i + } + } + if (factors[pivotRow][k] === 0) { + return undefined + } + if (pivotRow !== k) { + [factors[k], factors[pivotRow]] = [factors[pivotRow], factors[k]]; + [permutation[k], permutation[pivotRow]] = [permutation[pivotRow], permutation[k]] + sign = -sign + } + for (let i = k + 1; i < size; i++) { + const multiplier = factors[i][k] / factors[k][k] + factors[i][k] = multiplier + for (let j = k + 1; j < size; j++) { + factors[i][j] -= multiplier * factors[k][j] + } + } + } + + return {factors, permutation, sign} +} + +/** + * Calculates the determinant as the product of the pivots of the LU decomposition. A singular matrix whose + * elimination meets an exact zero pivot gives 0; otherwise the result carries the rounding error of the elimination. + * + * @param matrix - a square matrix + */ +function determinant(matrix: number[][]): number { + const decomposition = luDecomposition(matrix) + if (decomposition === undefined) { + return 0 + } + return decomposition.factors.reduce((product, row, index) => product * row[index], decomposition.sign) +} + +/** + * 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 { + 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 +} + +/** + * Builds the identity matrix. + * + * @param size - the number of rows and columns + */ +function identityMatrix(size: number): number[][] { + return Array.from({length: size}, (_, row) => Array.from({length: size}, (_unused, column) => row === column ? 1 : 0)) +} + +/** + * Calculates the inverse from the LU decomposition, solving one triangular system pair per column of the identity matrix. + * + * @param decomposition - the LU decomposition of a non-singular matrix + */ +function inverse(decomposition: LuDecomposition): number[][] { + const {factors, permutation} = decomposition + const size = factors.length + const result = identityMatrix(size) + + for (let column = 0; column < size; column++) { + const solution = permutation.map((originalRow) => originalRow === column ? 1 : 0) + for (let i = 0; i < size; i++) { + for (let j = 0; j < i; j++) { + solution[i] -= factors[i][j] * solution[j] + } + } + for (let i = size - 1; i >= 0; i--) { + for (let j = i + 1; j < size; j++) { + solution[i] -= factors[i][j] * solution[j] + } + solution[i] /= factors[i][i] + } + solution.forEach((value, row) => { + result[row][column] = value + }) + } + + return result +} + export class MatrixPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { 'MMULT': { @@ -98,6 +253,29 @@ export class MatrixPlugin extends FunctionPlugin implements FunctionPluginTypech ], vectorizationForbidden: true, }, + 'MUNIT': { + method: 'munit', + sizeOfResultArrayMethod: 'munitArraySize', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER}, + ], + vectorizationForbidden: true, + }, + 'MDETERM': { + method: 'mdeterm', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + ], + vectorizationForbidden: true, + }, + 'MINVERSE': { + method: 'minverse', + sizeOfResultArrayMethod: 'minverseArraySize', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + ], + vectorizationForbidden: true, + }, 'MAXPOOL': { method: 'maxpool', sizeOfResultArrayMethod: 'maxpoolArraySize', @@ -152,6 +330,115 @@ export class MatrixPlugin extends FunctionPlugin implements FunctionPluginTypech return arraySizeForMultiplication(left, right) } + /** + * Corresponds to MUNIT(Dimension). + * + * Returns the identity matrix of `Dimension` x `Dimension` cells. The dimension is coerced to a number, so a + * numeric text such as "3" is accepted, and a fractional dimension is truncated. A dimension below 1, text that is + * not a number, or a dimension above the row or column limit of a sheet returns the #VALUE! error. + */ + public munit(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('MUNIT'), (dimension: number) => { + const size = Math.trunc(dimension) + if (!(size >= 1)) { + return new CellError(ErrorType.VALUE, ErrorMessage.LessThanOne) + } + if (size > this.largestIdentityMatrix()) { + return new CellError(ErrorType.VALUE, ErrorMessage.ValueLarge) + } + return SimpleRangeValue.onlyNumbers(identityMatrix(size)) + }) + } + + public munitArraySize(ast: ProcedureAst): ArraySize { + if (ast.args.length !== 1) { + return ArraySize.error() + } + const argument = ast.args[0] + const dimension = argument.type === AstNodeType.STRING ? this.textLiteralAsNumber(argument.value) : numberLiteral(argument) + if (dimension === undefined) { + return ArraySize.scalar() + } + const size = Math.trunc(dimension) + return size >= 1 && size <= this.largestIdentityMatrix() ? new ArraySize(size, size) : ArraySize.error() + } + + /** + * Coerces a text literal to a number the way a NUMBER argument is coerced at evaluation, so that the predicted + * result size agrees with the evaluated one, including the configured decimal separator. + * + * @param text - the text of the literal + * @returns the number, or undefined when the text is not a number + */ + private textLiteralAsNumber(text: string): Maybe { + const coerced = this.coerceScalarToNumberOrError(text) + return isExtendedNumber(coerced) ? getRawValue(coerced) : undefined + } + + /** + * The largest dimension `MUNIT` accepts: the identity matrix must fit within both the row and the column limit of a sheet. + */ + private largestIdentityMatrix(): number { + return Math.min(this.config.maxRows, this.config.maxColumns) + } + + /** + * Corresponds to MDETERM(Array). + * + * Returns the determinant of a square matrix of numbers. A non-square matrix or a non-numeric value + * returns the #VALUE! error, and a determinant too large for a number returns the #NUM! error. + */ + 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) + } + const decomposition = luDecomposition(numbers) + if (decomposition === undefined) { + return new CellError(ErrorType.NUM, ErrorMessage.SingularMatrix) + } + const result = inverse(decomposition) + if (!result.every((row) => row.every(Number.isFinite))) { + return new CellError(ErrorType.NUM, ErrorMessage.NaN) + } + return SimpleRangeValue.onlyNumbers(result) + }) + } + + public minverseArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + if (ast.args.length !== 1) { + return ArraySize.error() + } + const metadata = this.metadata('MINVERSE') + const size = this.arraySizeForAst(ast.args[0], new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false))) + return size.width === size.height ? new ArraySize(size.width, size.height) : ArraySize.error() + } + /** * Corresponds to MAXPOOL(Range, Window_size, Stride). * diff --git a/hyperformula/src/license/functionCapabilities.ts b/hyperformula/src/license/functionCapabilities.ts index a26733ec5f..74d947d62b 100644 --- a/hyperformula/src/license/functionCapabilities.ts +++ b/hyperformula/src/license/functionCapabilities.ts @@ -90,7 +90,7 @@ const UNGROUPED_FUNCTIONS = [ 'IMEXP', 'IMLN', 'IMLOG10', 'IMLOG2', 'IMPOWER', 'IMPRODUCT', 'IMREAL', 'IMSEC', 'IMSECH', 'IMSIN', 'IMSINH', 'IMSQRT', 'IMSUB', 'IMSUM', 'IMTAN', 'INTERVAL', 'ISBINARY', 'ISFORMULA', 'ISNONTEXT', 'ISPMT', 'ISREF', 'LCM', 'LOG10', 'LOGNORM.DIST', 'LOGNORM.INV', 'MAXA', 'MAXPOOL', 'MEDIANPOOL', 'MINA', 'MIRR', - 'MMULT', 'MULTINOMIAL', 'NEGBINOM.DIST', 'NETWORKDAYS.INTL', 'NOMINAL', 'NORM.DIST', 'NORM.INV', + 'MDETERM', 'MINVERSE', 'MMULT', 'MULTINOMIAL', 'MUNIT', 'NEGBINOM.DIST', 'NETWORKDAYS.INTL', 'NOMINAL', 'NORM.DIST', 'NORM.INV', 'NORM.S.DIST', 'NORM.S.INV', 'NPER', 'OCT2BIN', 'OCT2DEC', 'OCT2HEX', 'PDURATION', 'PERCENTILE.EXC', 'PHI', 'POISSON.DIST', 'QUARTILE.EXC', 'QUARTILE.INC', 'RADIANS', 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', 'SERIESSUM', 'SHEET', 'SHEETS', 'SINH', 'SKEW', 'SKEW.P', 'SLOPE', 'SPLIT', 'SQRTPI', 'STANDARDIZE',