diff --git a/CHANGELOG.md b/CHANGELOG.md index b68c77419b..4f7e9ff491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ 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 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..fcb2b2a91a 100644 --- a/src/interpreter/plugin/MathPlugin.ts +++ b/src/interpreter/plugin/MathPlugin.ts @@ -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,32 @@ export class MathPlugin extends FunctionPlugin implements FunctionPluginTypechec ) } + public permut(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + 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 && Number.isFinite(ret); 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..786446d48d 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,23 @@ 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) + } + if (trials > MAX_BINOMIAL_TRIALS) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + return binomialRangeProbability(trials, prob, succ, succUpper) + } + ) + } + public binomialinv(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('BINOM.INV'), (trials: number, prob: number, alpha: number) => { @@ -841,3 +867,143 @@ 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. + */ +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 + } +} 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',