From d4c82746f90e4cf6bbac38fbb49d40780f916d26 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 16:26:53 +0000 Subject: [PATCH 1/8] Add PERMUT, PERMUTATIONA and BINOM.DIST.RANGE functions Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + src/i18n/languages/csCZ.ts | 3 ++ src/i18n/languages/daDK.ts | 3 ++ src/i18n/languages/deDE.ts | 3 ++ src/i18n/languages/enGB.ts | 3 ++ src/i18n/languages/esES.ts | 3 ++ src/i18n/languages/fiFI.ts | 3 ++ src/i18n/languages/frFR.ts | 3 ++ src/i18n/languages/huHU.ts | 3 ++ src/i18n/languages/idID.ts | 3 ++ src/i18n/languages/itIT.ts | 3 ++ src/i18n/languages/nbNO.ts | 3 ++ src/i18n/languages/nlNL.ts | 3 ++ src/i18n/languages/plPL.ts | 3 ++ src/i18n/languages/ptPT.ts | 3 ++ src/i18n/languages/ruRU.ts | 3 ++ src/i18n/languages/svSE.ts | 3 ++ src/i18n/languages/trTR.ts | 3 ++ .../categories/math-and-trigonometry.ts | 14 ++++++ .../categories/statistical.ts | 7 +++ src/interpreter/plugin/MathPlugin.ts | 46 ++++++++++++++++++- src/interpreter/plugin/StatisticalPlugin.ts | 42 +++++++++++++++++ 22 files changed, 160 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b68c77419b..078ffeaec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added support for the new license key format. A proprietary key can now grant a subset of the library: a function your key does not include evaluates to a `#LIC!` error, and the matching parts of the API throw a `LicenseCapabilityMissingError`. `getAvailableFunctions()` and `getFunctionDetails()` describe only the functions your key includes. [#1728](https://github.com/handsontable/hyperformula/pull/1728) +- Added the `PERMUT`, `PERMUTATIONA` and `BINOM.DIST.RANGE` functions. ### Changed diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fea286083c..ad05d6fa7f 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FACTDOUBLE', COMBIN: 'KOMBINACE', COMBINA: 'COMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'GCD', LCM: 'LCM', MROUND: 'MROUND', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOM.DIST', BINOMDIST: 'BINOMDIST', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 9264551249..08aacca43f 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'DOBBELT.FAKULTET', COMBIN: 'KOMBIN', COMBINA: 'KOMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'STØRSTE.FÆLLES.DIVISOR', LCM: 'MINDSTE.FÆLLES.MULTIPLUM', MROUND: 'MAFRUND', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOMIAL.FORDELING', BINOMDIST: 'BINOMIALFORDELING', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOMIAL.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 4d3d39319b..6f0b40d387 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'ZWEIFAKULTÄT', COMBIN: 'KOMBINATIONEN', COMBINA: 'KOMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'GGT', LCM: 'KGV', MROUND: 'VRUNDEN', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOM.VERT', BINOMDIST: 'BINOMVERT', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732cd..268029cbf0 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -293,6 +293,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FACTDOUBLE', COMBIN: 'COMBIN', COMBINA: 'COMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'GCD', LCM: 'LCM', MROUND: 'MROUND', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOM.DIST', BINOMDIST: 'BINOMDIST', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 9c6b817b98..d9192bda7c 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -284,6 +284,8 @@ export const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FACT.DOBLE', COMBIN: 'COMBINAT', COMBINA: 'COMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'M.C.D', LCM: 'M.C.M', MROUND: 'REDOND.MULT', @@ -314,6 +316,7 @@ export const dictionary: RawTranslationPackage = { BETAINV: 'DISTR.BETA.INV', 'BINOM.DIST': 'DISTR.BINOM.N', BINOMDIST: 'DISTR.BINOM', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'INV.BINOM', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 99ff05708f..22b0226837 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'KERTOMA.OSA', COMBIN: 'KOMBINAATIO', COMBINA: 'KOMBINAATIOA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'SUURIN.YHT.TEKIJÄ', LCM: 'PIENIN.YHT.JAETTAVA', MROUND: 'PYÖRISTÄ.KERR', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BEETAJAKAUMA.KÄÄNT', 'BINOM.DIST': 'BINOMI.JAKAUMA', BINOMDIST: 'BINOMIJAKAUMA', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOMIJAKAUMA.KÄÄNT', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index f7b9be45f7..afbe1c8447 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FACTDOUBLE', COMBIN: 'COMBIN', COMBINA: 'COMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'PGCD', LCM: 'PPCM', MROUND: 'ARRONDI.AU.MULTIPLE', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETA.INVERSE', 'BINOM.DIST': 'LOI.BINOMIALE.N', BINOMDIST: 'LOI.BINOMIALE', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'LOI.BINOMIALE.INVERSE', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e40219553a..ba81c7d2fa 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FAKTDUPLA', COMBIN: 'KOMBINÁCIÓK', COMBINA: 'KOMBINÁCIÓK.ISM', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'LKO', LCM: 'LKT', MROUND: 'TÖBBSZ.KEREKÍT', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'INVERZ.BÉTA', 'BINOM.DIST': 'BINOM.ELOSZL', BINOMDIST: 'BINOM.ELOSZLÁS', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INVERZ', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db6..d6c8d6c01b 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -293,6 +293,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FAKTOR.GANDA', COMBIN: 'KOMBINASI', COMBINA: 'KOMBINASIA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'FPB', LCM: 'KPK', MROUND: 'BULATKAN.MULTIPLE', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOM.DIST', BINOMDIST: 'BINOMDIST', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1a494628f8..faaa785834 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FATT.DOPPIO', COMBIN: 'COMBINAZIONE', COMBINA: 'COMBINAZIONE.VALORI', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'MCD', LCM: 'MCM', MROUND: 'ARROTONDA.MULTIPLO', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'INV.BETA', 'BINOM.DIST': 'DISTRIB.BINOM.N', BINOMDIST: 'DISTRIB.BINOM', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'INV.BINOM', BESSELI: 'BESSEL.I', BESSELJ: 'BESSEL.J', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index ff15415a98..332633c9f2 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'DOBBELFAKT', COMBIN: 'KOMBINASJON', COMBINA: 'KOMBINASJONA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'SFF', LCM: 'MFM', MROUND: 'MRUND', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'INVERS.BETA.FORDELING', 'BINOM.DIST': 'BINOM.FORDELING.N', BINOMDIST: 'BINOM.FORDELING', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index c491e2f44b..4e20a67d66 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'DUBBELE.FACULTEIT', COMBIN: 'COMBINATIES', COMBINA: 'COMBIN.A', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'GGD', LCM: 'KGV', MROUND: 'AFRONDEN.N.VEELVOUD', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOM.VERD', BINOMDIST: 'BINOMIALE.VERD', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOMIALE.INV', BESSELI: 'BESSEL.I', BESSELJ: 'BESSEL.J', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 6fa4f016b4..ea233bdf4e 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'SILNIA.DWUKR', COMBIN: 'KOMBINACJE', COMBINA: 'KOMBINACJE.A', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'NAJW.WSP.DZIEL', LCM: 'NAJMN.WSP.WIEL', MROUND: 'ZAOKR.DO.WIELOKR', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'ROZKŁAD.BETA.ODW', 'BINOM.DIST': 'ROZKŁ.DWUM', BINOMDIST: 'ROZKŁAD.DWUM', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'ROZKŁ.DWUM.ODWR', BESSELI: 'BESSEL.I', BESSELJ: 'BESSEL.J', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 6c1e68c328..28b063c025 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'FATDUPLO', COMBIN: 'COMBIN', COMBINA: 'COMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'MDC', LCM: 'MMC', MROUND: 'MARRED', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETA.ACUM.INV', 'BINOM.DIST': 'DISTR.BINOM', BINOMDIST: 'DISTRBINOM', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'INV.BINOM', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6b35c1ce2f..8d533fd5a2 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'ДВФАКТР', COMBIN: 'ЧИСЛКОМБ', COMBINA: 'ЧИСЛКОМБА', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'НОД', LCM: 'НОК', MROUND: 'ОКРУГЛТ', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'БЕТАОБР', 'BINOM.DIST': 'БИНОМ.РАСП', BINOMDIST: 'БИНОМРАСП', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'БИНОМ.ОБР', BESSELI: 'БЕССЕЛЬ.I', BESSELJ: 'БЕССЕЛЬ.J', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f87580087..058f4787f0 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'DUBBELFAKULTET', COMBIN: 'KOMBIN', COMBINA: 'COMBINA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'SGD', LCM: 'MGM', MROUND: 'MAVRUNDA', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETAINV', 'BINOM.DIST': 'BINOM.FÖRD', BINOMDIST: 'BINOMFÖRD', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BINOM.INV', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 93c2176d37..982b694311 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -284,6 +284,8 @@ const dictionary: RawTranslationPackage = { FACTDOUBLE: 'ÇİFTFAKTÖR', COMBIN: 'KOMBİNASYON', COMBINA: 'KOMBİNASYONA', + PERMUT: 'PERMUT', + PERMUTATIONA: 'PERMUTATIONA', GCD: 'OBEB', LCM: 'OKEK', MROUND: 'KYUVARLA', @@ -314,6 +316,7 @@ const dictionary: RawTranslationPackage = { BETAINV: 'BETATERS', 'BINOM.DIST': 'BİNOM.DAĞ', BINOMDIST: 'BİNOMDAĞ', + 'BINOM.DIST.RANGE': 'BINOM.DIST.RANGE', 'BINOM.INV': 'BİNOM.TERS', BESSELI: 'BESSELI', BESSELJ: 'BESSELJ', diff --git a/src/interpreter/functionMetadata/categories/math-and-trigonometry.ts b/src/interpreter/functionMetadata/categories/math-and-trigonometry.ts index e48408bcd1..974e1c8e60 100644 --- a/src/interpreter/functionMetadata/categories/math-and-trigonometry.ts +++ b/src/interpreter/functionMetadata/categories/math-and-trigonometry.ts @@ -129,6 +129,20 @@ export const MATH_AND_TRIGONOMETRY_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=COMBINA(4, 3)', '=COMBINA(10, 2)'], }, + PERMUT: { + category: 'Math and trigonometry', + shortDescription: 'Returns the number of permutations of a given number of items chosen from a set (without repetitions).', + parameters: [{name: 'number', description: 'The total number of items; truncated to an integer.'}, {name: 'number_chosen', description: 'The number of items in each permutation; truncated to an integer and must not exceed number.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=PERMUT(5, 2)', '=PERMUT(10, 3)'], + }, + PERMUTATIONA: { + category: 'Math and trigonometry', + shortDescription: 'Returns the number of permutations of a given number of items chosen from a set (with repetitions).', + parameters: [{name: 'number', description: 'The total number of items; truncated to an integer.'}, {name: 'number_chosen', description: 'The number of items in each permutation, where an item may repeat; truncated to an integer.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=PERMUTATIONA(3, 2)', '=PERMUTATIONA(2, 10)'], + }, COS: { category: 'Math and trigonometry', shortDescription: 'Returns the cosine of the given angle (in radians).', diff --git a/src/interpreter/functionMetadata/categories/statistical.ts b/src/interpreter/functionMetadata/categories/statistical.ts index 7a536e4cd2..89b37e4d1f 100644 --- a/src/interpreter/functionMetadata/categories/statistical.ts +++ b/src/interpreter/functionMetadata/categories/statistical.ts @@ -59,6 +59,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=BINOM.DIST(3, 10, 0.5, FALSE())', '=BINOM.DIST(3, 10, 0.5, TRUE())'], }, + 'BINOM.DIST.RANGE': { + category: 'Statistical', + shortDescription: 'Returns the probability of a trial result using a binomial distribution over a range of successes.', + parameters: [{name: 'trials', description: 'The total number of independent trials; truncated to an integer.'}, {name: 'probability_s', description: 'The probability of success on a single trial, between 0 and 1.'}, {name: 'number_s', description: 'The lower bound of the number of successes; truncated to an integer.'}, {name: 'number_s2', description: 'The upper bound of the number of successes; truncated to an integer. Defaults to number_s when omitted.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=BINOM.DIST.RANGE(10, 0.5, 5)', '=BINOM.DIST.RANGE(10, 0.5, 3, 7)'], + }, 'BINOM.INV': { category: 'Statistical', shortDescription: 'Returns inverse binomial distribution value.', diff --git a/src/interpreter/plugin/MathPlugin.ts b/src/interpreter/plugin/MathPlugin.ts index e783d801fb..7496512aae 100644 --- a/src/interpreter/plugin/MathPlugin.ts +++ b/src/interpreter/plugin/MathPlugin.ts @@ -5,7 +5,7 @@ import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' -import {ProcedureAst} from '../../parser' +import {AstNodeType, ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' import {InterpreterValue, RawInterpreterValue} from '../InterpreterValue' import {SimpleRangeValue} from '../../SimpleRangeValue' @@ -39,6 +39,20 @@ export class MathPlugin extends FunctionPlugin implements FunctionPluginTypechec {argumentType: FunctionArgumentType.NUMBER, minValue: 0} ] }, + 'PERMUT': { + method: 'permut', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER, minValue: 0}, + {argumentType: FunctionArgumentType.NUMBER, minValue: 0} + ] + }, + 'PERMUTATIONA': { + method: 'permutationa', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER, minValue: 0}, + {argumentType: FunctionArgumentType.NUMBER, minValue: 0} + ] + }, 'GCD': { method: 'gcd', parameters: [ @@ -167,6 +181,36 @@ export class MathPlugin extends FunctionPlugin implements FunctionPluginTypechec ) } + public permut(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + // whole-column and whole-row ranges are not implicitly intersected in a scalar slot + if (ast.args.some(arg => arg.type === AstNodeType.COLUMN_RANGE || arg.type === AstNodeType.ROW_RANGE)) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + return this.runFunction(ast.args, state, this.metadata('PERMUT'), + (n: number, k: number) => { + n = Math.trunc(n) + k = Math.trunc(k) + if (k > n) { + return new CellError(ErrorType.NUM, ErrorMessage.WrongOrder) + } + let ret = 1 + for (let i = 0; i < k; i++) { + ret *= n - i + } + return Number.isFinite(ret) ? ret : new CellError(ErrorType.NUM, ErrorMessage.NaN) + } + ) + } + + public permutationa(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('PERMUTATIONA'), + (n: number, k: number) => { + const ret = Math.pow(Math.trunc(n), Math.trunc(k)) + return Number.isFinite(ret) ? ret : new CellError(ErrorType.NUM, ErrorMessage.NaN) + } + ) + } + public gcd(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('GCD'), (...args: RawInterpreterValue[]) => { diff --git a/src/interpreter/plugin/StatisticalPlugin.ts b/src/interpreter/plugin/StatisticalPlugin.ts index 54fd6e2dbc..4e0155111c 100644 --- a/src/interpreter/plugin/StatisticalPlugin.ts +++ b/src/interpreter/plugin/StatisticalPlugin.ts @@ -132,6 +132,15 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT {argumentType: FunctionArgumentType.BOOLEAN}, ] }, + 'BINOM.DIST.RANGE': { + method: 'binomialdistrange', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER, minValue: 0}, + {argumentType: FunctionArgumentType.NUMBER, minValue: 0, maxValue: 1}, + {argumentType: FunctionArgumentType.NUMBER, minValue: 0}, + {argumentType: FunctionArgumentType.NUMBER, optionalArg: true, minValue: 0}, + ] + }, 'BINOM.INV': { method: 'binomialinv', parameters: [ @@ -541,6 +550,39 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT ) } + public binomialdistrange(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('BINOM.DIST.RANGE'), + (trials: number, prob: number, succ: number, succUpper?: number) => { + trials = Math.trunc(trials) + succ = Math.trunc(succ) + succUpper = succUpper === undefined ? succ : Math.trunc(succUpper) + if (succ > succUpper || succUpper > trials) { + return new CellError(ErrorType.NUM, ErrorMessage.WrongOrder) + } + let sum = 0 + if (prob === 0 || prob === 1) { + for (let i = succ; i <= succUpper; i++) { + sum += binomial.pdf(i, trials, prob) + } + return sum + } + // work in log space: the factorial-based pdf loses precision for large numbers of trials + const m = Math.min(succ, trials - succ) + let logCombination = 0 + for (let j = 1; j <= m; j++) { + logCombination += Math.log((trials - m + j) / j) + } + const logRatio = Math.log(prob) - Math.log1p(-prob) + let logPmf = logCombination + succ * Math.log(prob) + (trials - succ) * Math.log1p(-prob) + for (let i = succ; i <= succUpper; i++) { + sum += Math.exp(logPmf) + logPmf += Math.log((trials - i) / (i + 1)) + logRatio + } + return sum + } + ) + } + public binomialinv(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('BINOM.INV'), (trials: number, prob: number, alpha: number) => { From e01043ee3ce7bc19321ef0f337fa4af88526abcd Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 16:26:53 +0000 Subject: [PATCH 2/8] docs: link the PR in the CHANGELOG entry Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078ffeaec1..6df0d72a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added support for the new license key format. A proprietary key can now grant a subset of the library: a function your key does not include evaluates to a `#LIC!` error, and the matching parts of the API throw a `LicenseCapabilityMissingError`. `getAvailableFunctions()` and `getFunctionDetails()` describe only the functions your key includes. [#1728](https://github.com/handsontable/hyperformula/pull/1728) -- Added the `PERMUT`, `PERMUTATIONA` and `BINOM.DIST.RANGE` functions. +- Added the `PERMUT`, `PERMUTATIONA` and `BINOM.DIST.RANGE` functions. [#1790](https://github.com/handsontable/hyperformula/pull/1790) ### Changed From 09f18a124e139d3e590c5e7cc0d7b85ef8b80908 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 6 Oct 2026 00:12:57 +0000 Subject: [PATCH 3/8] Do not special-case whole-column and whole-row ranges in PERMUT They are intersected like for PERMUTATIONA and COMBIN; Excel answers #SPILL! for all of them. Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/MathPlugin.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/interpreter/plugin/MathPlugin.ts b/src/interpreter/plugin/MathPlugin.ts index 7496512aae..0274b4e10a 100644 --- a/src/interpreter/plugin/MathPlugin.ts +++ b/src/interpreter/plugin/MathPlugin.ts @@ -5,7 +5,7 @@ import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' -import {AstNodeType, ProcedureAst} from '../../parser' +import {ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' import {InterpreterValue, RawInterpreterValue} from '../InterpreterValue' import {SimpleRangeValue} from '../../SimpleRangeValue' @@ -182,10 +182,6 @@ export class MathPlugin extends FunctionPlugin implements FunctionPluginTypechec } public permut(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - // whole-column and whole-row ranges are not implicitly intersected in a scalar slot - if (ast.args.some(arg => arg.type === AstNodeType.COLUMN_RANGE || arg.type === AstNodeType.ROW_RANGE)) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } return this.runFunction(ast.args, state, this.metadata('PERMUT'), (n: number, k: number) => { n = Math.trunc(n) From 4feaae9be81576c213802fef3c6e1af636b8bda9 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 6 Oct 2026 00:20:12 +0000 Subject: [PATCH 4/8] Stop PERMUT iterating once the product overflows PERMUT(1000000000, 1000000000) took 1.3 s to answer #NUM!; Excel answers at once. Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/MathPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter/plugin/MathPlugin.ts b/src/interpreter/plugin/MathPlugin.ts index 0274b4e10a..fcb2b2a91a 100644 --- a/src/interpreter/plugin/MathPlugin.ts +++ b/src/interpreter/plugin/MathPlugin.ts @@ -190,7 +190,7 @@ export class MathPlugin extends FunctionPlugin implements FunctionPluginTypechec return new CellError(ErrorType.NUM, ErrorMessage.WrongOrder) } let ret = 1 - for (let i = 0; i < k; i++) { + for (let i = 0; i < k && Number.isFinite(ret); i++) { ret *= n - i } return Number.isFinite(ret) ? ret : new CellError(ErrorType.NUM, ErrorMessage.NaN) From 963184a6c646f6539533646446dc898a49b69848 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 6 Oct 2026 03:50:52 +0000 Subject: [PATCH 5/8] Keep BINOM.DIST.RANGE accurate for large numbers of trials Neumaier-compensated sums for the log of the binomial coefficient, the running log probability and the total, and the result is capped at 1. At 100000 trials the relative error against exact values drops from about 1e-9 to 1e-11, and a full range no longer comes out above 1. Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/StatisticalPlugin.ts | 44 ++++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/interpreter/plugin/StatisticalPlugin.ts b/src/interpreter/plugin/StatisticalPlugin.ts index 4e0155111c..53972e371a 100644 --- a/src/interpreter/plugin/StatisticalPlugin.ts +++ b/src/interpreter/plugin/StatisticalPlugin.ts @@ -559,26 +559,32 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT if (succ > succUpper || succUpper > trials) { return new CellError(ErrorType.NUM, ErrorMessage.WrongOrder) } - let sum = 0 if (prob === 0 || prob === 1) { + let degenerate = 0 for (let i = succ; i <= succUpper; i++) { - sum += binomial.pdf(i, trials, prob) + degenerate += binomial.pdf(i, trials, prob) } - return sum + return Math.min(degenerate, 1) } - // work in log space: the factorial-based pdf loses precision for large numbers of trials + // Work in log space (the factorial-based pdf overflows for large numbers of trials) and add with Neumaier + // compensation: a plain running sum over up to millions of terms loses digits, and at 10^7 trials it pushed the + // total of a full range above 1. const m = Math.min(succ, trials - succ) - let logCombination = 0 + const logCombination = new CompensatedSum() for (let j = 1; j <= m; j++) { - logCombination += Math.log((trials - m + j) / j) + logCombination.add(Math.log((trials - m + j) / j)) } const logRatio = Math.log(prob) - Math.log1p(-prob) - let logPmf = logCombination + succ * Math.log(prob) + (trials - succ) * Math.log1p(-prob) + const logPmf = new CompensatedSum() + logPmf.add(logCombination.value()) + logPmf.add(succ * Math.log(prob)) + logPmf.add((trials - succ) * Math.log1p(-prob)) + const sum = new CompensatedSum() for (let i = succ; i <= succUpper; i++) { - sum += Math.exp(logPmf) - logPmf += Math.log((trials - i) / (i + 1)) + logRatio + sum.add(Math.exp(logPmf.value())) + logPmf.add(Math.log((trials - i) / (i + 1)) + logRatio) } - return sum + return Math.min(sum.value(), 1) } ) } @@ -883,3 +889,21 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT } } +/** + * Neumaier's compensated summation: keeps the rounding error of each addition in a separate term, so the error of a + * sum of n terms stays near one rounding instead of growing with n. + */ +class CompensatedSum { + private sum = 0 + private compensation = 0 + + public add(term: number): void { + const total = this.sum + term + this.compensation += Math.abs(this.sum) >= Math.abs(term) ? (this.sum - total) + term : (term - total) + this.sum + this.sum = total + } + + public value(): number { + return this.sum + this.compensation + } +} From bc94b97ed4086a00cda9b273a452603d91f263d2 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 6 Oct 2026 08:53:24 +0000 Subject: [PATCH 6/8] Compute BINOM.DIST.RANGE with Loader's saddle-point probability The terms are summed outwards from the mode and stop once they no longer change the sum, so the cost grows with the square root of the number of trials instead of the number of trials, and the result agrees with Excel to about 3e-11 up to the limit Excel has (2147483646 trials; more is #NUM!). Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/StatisticalPlugin.ts | 150 ++++++++++++++++---- 1 file changed, 125 insertions(+), 25 deletions(-) diff --git a/src/interpreter/plugin/StatisticalPlugin.ts b/src/interpreter/plugin/StatisticalPlugin.ts index 53972e371a..786446d48d 100644 --- a/src/interpreter/plugin/StatisticalPlugin.ts +++ b/src/interpreter/plugin/StatisticalPlugin.ts @@ -559,32 +559,10 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT if (succ > succUpper || succUpper > trials) { return new CellError(ErrorType.NUM, ErrorMessage.WrongOrder) } - if (prob === 0 || prob === 1) { - let degenerate = 0 - for (let i = succ; i <= succUpper; i++) { - degenerate += binomial.pdf(i, trials, prob) - } - return Math.min(degenerate, 1) - } - // Work in log space (the factorial-based pdf overflows for large numbers of trials) and add with Neumaier - // compensation: a plain running sum over up to millions of terms loses digits, and at 10^7 trials it pushed the - // total of a full range above 1. - const m = Math.min(succ, trials - succ) - const logCombination = new CompensatedSum() - for (let j = 1; j <= m; j++) { - logCombination.add(Math.log((trials - m + j) / j)) - } - const logRatio = Math.log(prob) - Math.log1p(-prob) - const logPmf = new CompensatedSum() - logPmf.add(logCombination.value()) - logPmf.add(succ * Math.log(prob)) - logPmf.add((trials - succ) * Math.log1p(-prob)) - const sum = new CompensatedSum() - for (let i = succ; i <= succUpper; i++) { - sum.add(Math.exp(logPmf.value())) - logPmf.add(Math.log((trials - i) / (i + 1)) + logRatio) + if (trials > MAX_BINOMIAL_TRIALS) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) } - return Math.min(sum.value(), 1) + return binomialRangeProbability(trials, prob, succ, succUpper) } ) } @@ -889,6 +867,128 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT } } +/** + * The largest number of trials BINOM.DIST.RANGE accepts. Excel answers #NUM! from 2147483647 trials upwards + * (measured: 2147483646 works, 2147483647 does not). + */ +const MAX_BINOMIAL_TRIALS = 2147483646 + +/** Terms of the Stirling series for the error of the Stirling approximation of the factorial. */ +const STIRLING_S0 = 1 / 12 +const STIRLING_S1 = 1 / 360 +const STIRLING_S2 = 1 / 1260 +const STIRLING_S3 = 1 / 1680 +const STIRLING_S4 = 1 / 1188 + +/** + * The error of Stirling's approximation, log(n!) - ((n + 1/2) log(n) - n + log(sqrt(2 pi))), for n >= 15. + * With bd0 it gives binomial probabilities without the cancellation of the textbook formula (Loader, 2000). + */ +function stirlingError(n: number): number { + const nn = n * n + if (n > 500) { + return (STIRLING_S0 - STIRLING_S1 / nn) / n + } + if (n > 80) { + return (STIRLING_S0 - (STIRLING_S1 - STIRLING_S2 / nn) / nn) / n + } + if (n > 35) { + return (STIRLING_S0 - (STIRLING_S1 - (STIRLING_S2 - STIRLING_S3 / nn) / nn) / nn) / n + } + return (STIRLING_S0 - (STIRLING_S1 - (STIRLING_S2 - (STIRLING_S3 - STIRLING_S4 / nn) / nn) / nn) / nn) / n +} + +/** + * The deviance term x log(x / np) + np - x, evaluated without losing digits when x is close to np. + */ +function deviance(x: number, np: number): number { + if (Math.abs(x - np) < 0.1 * (x + np)) { + let v = (x - np) / (x + np) + let sum = (x - np) * v + let term = 2 * x * v + v *= v + for (let j = 1; j < 1000; j++) { + term *= v + const next = sum + term / (2 * j + 1) + if (next === sum) { + return next + } + sum = next + } + return sum + } + return x * Math.log(x / np) + np - x +} + +/** + * The probability of exactly k successes in n trials with success probability p (and q = 1 - p). + */ +function binomialPmf(k: number, n: number, p: number, q: number): number { + if (k === 0) { + return Math.exp(n * Math.log1p(-p)) + } + if (k === n) { + return Math.exp(n * Math.log(p)) + } + if (k >= 15 && n - k >= 15) { + const logPmf = stirlingError(n) - stirlingError(k) - stirlingError(n - k) - deviance(k, n * p) - deviance(n - k, n * q) + return Math.exp(logPmf) / Math.sqrt(2 * Math.PI * k * (n - k) / n) + } + // Few successes or few failures: the binomial coefficient has at most 14 factors. + const m = Math.min(k, n - k) + let logCoefficient = 0 + for (let j = 1; j <= m; j++) { + logCoefficient += Math.log((n - m + j) / j) + } + return Math.exp(logCoefficient + k * Math.log(p) + (n - k) * Math.log1p(-p)) +} + +/** + * The probability of between lo and hi successes (inclusive) in n trials. + * + * Starts at the term closest to the mode, where the probabilities are largest, and walks outwards with the ratio of + * neighbouring terms until the terms no longer change the sum. Binomial terms fall off like a Gaussian, so only about + * ten standard deviations are visited however large n is: the cost grows with the square root of n, not with n. + */ +function binomialRangeProbability(n: number, p: number, lo: number, hi: number): number { + if (p === 0) { + return lo === 0 ? 1 : 0 + } + if (p === 1) { + return hi === n ? 1 : 0 + } + const q = 1 - p + const mode = Math.min(n, Math.floor((n + 1) * p)) + const start = Math.min(Math.max(mode, lo), hi) + const first = binomialPmf(start, n, p, q) + if (first === 0) { + return 0 + } + const sum = new CompensatedSum() + sum.add(first) + + let term = first + for (let i = start; i < hi; i++) { + term *= (n - i) / (i + 1) * (p / q) + sum.add(term) + if (term === 0 || term < sum.value() * NEGLIGIBLE_TERM) { + break + } + } + term = first + for (let i = start; i > lo; i--) { + term *= i / (n - i + 1) * (q / p) + sum.add(term) + if (term === 0 || term < sum.value() * NEGLIGIBLE_TERM) { + break + } + } + return Math.min(sum.value(), 1) +} + +/** A term this much smaller than the running sum no longer changes it in double precision. */ +const NEGLIGIBLE_TERM = 1e-18 + /** * Neumaier's compensated summation: keeps the rounding error of each addition in a separate term, so the error of a * sum of n terms stays near one rounding instead of growing with n. From adc9373b61417dd1803d954ac184e697fb3084cc Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 8 Oct 2026 03:55:39 +0000 Subject: [PATCH 7/8] Register BINOM.DIST.RANGE PERMUT PERMUTATIONA in the license capability table Co-Authored-By: Claude Sonnet 5.5 --- src/license/functionCapabilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/license/functionCapabilities.ts b/src/license/functionCapabilities.ts index a26733ec5f..649cb7e6da 100644 --- a/src/license/functionCapabilities.ts +++ b/src/license/functionCapabilities.ts @@ -76,7 +76,7 @@ const FUNCTION_GROUPS: ReadonlyMap = new Map([ */ const UNGROUPED_FUNCTIONS = [ 'ACOSH', 'ACOT', 'ACOTH', 'ARABIC', 'ASINH', 'ATANH', 'AVEDEV', 'BASE', 'BESSELI', 'BESSELJ', 'BESSELK', - 'BESSELY', 'BETA.DIST', 'BETA.INV', 'BIN2DEC', 'BIN2HEX', 'BIN2OCT', 'BINOM.DIST', 'BINOM.INV', 'BITAND', + 'BESSELY', 'BETA.DIST', 'BETA.INV', 'BIN2DEC', 'BIN2HEX', 'BIN2OCT', 'BINOM.DIST', 'BINOM.DIST.RANGE', 'BINOM.INV', 'BITAND', 'BITLSHIFT', 'BITOR', 'BITRSHIFT', 'BITXOR', 'CEILING.MATH', 'CEILING.PRECISE', 'CHISQ.DIST', 'CHISQ.DIST.RT', 'CHISQ.INV', 'CHISQ.INV.RT', 'CHISQ.TEST', 'COMBIN', 'COMBINA', 'COMPLEX', 'CONFIDENCE.NORM', 'CONFIDENCE.T', 'CORREL', 'COSH', 'COT', 'COTH', 'COUNTUNIQUE', 'COVARIANCE.P', @@ -92,7 +92,7 @@ const UNGROUPED_FUNCTIONS = [ 'ISREF', 'LCM', 'LOG10', 'LOGNORM.DIST', 'LOGNORM.INV', 'MAXA', 'MAXPOOL', 'MEDIANPOOL', 'MINA', 'MIRR', 'MMULT', 'MULTINOMIAL', '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', + 'PERMUT', 'PERMUTATIONA', 'PHI', 'POISSON.DIST', 'QUARTILE.EXC', 'QUARTILE.INC', 'RADIANS', 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', 'SERIESSUM', 'SHEET', 'SHEETS', 'SINH', 'SKEW', 'SKEW.P', 'SLOPE', 'SPLIT', 'SQRTPI', 'STANDARDIZE', 'STEYX', 'SUMX2MY2', 'SUMX2PY2', 'SYD', 'T.DIST', 'T.DIST.2T', 'T.DIST.RT', 'T.INV', 'T.INV.2T', 'T.TEST', 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'UNICODE', 'VARA', 'VARPA', From 58806511dbc4b2bf86c02ee720622df1c475a662 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 12 Oct 2026 03:46:04 +0000 Subject: [PATCH 8/8] docs: place the changelog entry under Unreleased Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6df0d72a39..4f7e9ff491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added the `PERMUT`, `PERMUTATIONA` and `BINOM.DIST.RANGE` functions. [#1790](https://github.com/handsontable/hyperformula/pull/1790) + ## [3.5.0] - 2026-10-09 ### Added - Added support for the new license key format. A proprietary key can now grant a subset of the library: a function your key does not include evaluates to a `#LIC!` error, and the matching parts of the API throw a `LicenseCapabilityMissingError`. `getAvailableFunctions()` and `getFunctionDetails()` describe only the functions your key includes. [#1728](https://github.com/handsontable/hyperformula/pull/1728) -- Added the `PERMUT`, `PERMUTATIONA` and `BINOM.DIST.RANGE` functions. [#1790](https://github.com/handsontable/hyperformula/pull/1790) ### Changed