diff --git a/CHANGELOG.md b/CHANGELOG.md index 323c746b2d..407e74b0c4 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 statistical functions `KURT`, `MODE.SNGL` (with the `MODE` alias), `PERCENTRANK.EXC` and `TRIMMEAN`. [#1800](https://github.com/handsontable/hyperformula/pull/1800) ### Fixed diff --git a/hyperformula/src/error-message.ts b/hyperformula/src/error-message.ts index d42040901c..68ce600991 100644 --- a/hyperformula/src/error-message.ts +++ b/hyperformula/src/error-message.ts @@ -49,6 +49,7 @@ export class ErrorMessage { public static TwoValues = 'Range needs to contain at least two elements.' public static ThreeValues = 'Range needs to contain at least three elements.' public static EqualXValues = 'All x values are equal.' + public static FourValues = 'Range needs to contain at least four elements.' public static IndexBounds = 'Index out of bounds.' public static IndexLarge = 'Index too large.' public static Formula = 'Expected formula.' diff --git a/hyperformula/src/i18n/languages/csCZ.ts b/hyperformula/src/i18n/languages/csCZ.ts index fea286083c..1e75c19e64 100644 --- a/hyperformula/src/i18n/languages/csCZ.ts +++ b/hyperformula/src/i18n/languages/csCZ.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INC', 'PERCENTILE.EXC': 'PERCENTIL.EXC', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'QUARTIL', 'QUARTILE.INC': 'QUARTIL.INC', 'QUARTILE.EXC': 'QUARTIL.EXC', diff --git a/hyperformula/src/i18n/languages/daDK.ts b/hyperformula/src/i18n/languages/daDK.ts index 9264551249..774f2768cb 100644 --- a/hyperformula/src/i18n/languages/daDK.ts +++ b/hyperformula/src/i18n/languages/daDK.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'FRAKTIL', 'PERCENTILE.INC': 'FRAKTIL.MEDTAG', 'PERCENTILE.EXC': 'FRAKTIL.UDELAD', + 'PERCENTRANK.EXC': 'PROCENTPLADS.UDELAD', + KURT: 'TOPSTEJL', + MODE: 'HYPPIGST', + 'MODE.SNGL': 'HYPPIGST.ENKELT', + TRIMMEAN: 'TRIMMIDDELVÆRDI', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.MEDTAG', 'QUARTILE.EXC': 'KVARTIL.UDELAD', diff --git a/hyperformula/src/i18n/languages/deDE.ts b/hyperformula/src/i18n/languages/deDE.ts index 4d3d39319b..f9236f546b 100644 --- a/hyperformula/src/i18n/languages/deDE.ts +++ b/hyperformula/src/i18n/languages/deDE.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'QUANTIL', 'PERCENTILE.INC': 'QUANTIL.INKL', 'PERCENTILE.EXC': 'QUANTIL.EXKL', + 'PERCENTRANK.EXC': 'QUANTILSRANG.EXKL', + KURT: 'KURT', + MODE: 'MODALWERT', + 'MODE.SNGL': 'MODUS.EINF', + TRIMMEAN: 'GESTUTZTMITTEL', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INKL', 'QUARTILE.EXC': 'QUARTILE.EXKL', diff --git a/hyperformula/src/i18n/languages/enGB.ts b/hyperformula/src/i18n/languages/enGB.ts index d271b732cd..01e9ba9ec9 100644 --- a/hyperformula/src/i18n/languages/enGB.ts +++ b/hyperformula/src/i18n/languages/enGB.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'PERCENTILE.INC', 'PERCENTILE.EXC': 'PERCENTILE.EXC', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INC', 'QUARTILE.EXC': 'QUARTILE.EXC', diff --git a/hyperformula/src/i18n/languages/esES.ts b/hyperformula/src/i18n/languages/esES.ts index 9c6b817b98..ff93dddb58 100644 --- a/hyperformula/src/i18n/languages/esES.ts +++ b/hyperformula/src/i18n/languages/esES.ts @@ -390,6 +390,11 @@ export const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INC', 'PERCENTILE.EXC': 'PERCENTIL.EXC', + 'PERCENTRANK.EXC': 'RANGO.PERCENTIL.EXC', + KURT: 'CURTOSIS', + MODE: 'MODA', + 'MODE.SNGL': 'MODA.UNO', + TRIMMEAN: 'MEDIA.ACOTADA', QUARTILE: 'CUARTIL', 'QUARTILE.INC': 'CUARTIL.INC', 'QUARTILE.EXC': 'CUARTIL.EXC', diff --git a/hyperformula/src/i18n/languages/fiFI.ts b/hyperformula/src/i18n/languages/fiFI.ts index 99ff05708f..a791edac29 100644 --- a/hyperformula/src/i18n/languages/fiFI.ts +++ b/hyperformula/src/i18n/languages/fiFI.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PROSENTTIPISTE', 'PERCENTILE.INC': 'PROSENTTIPISTE.SIS', 'PERCENTILE.EXC': 'PROSENTTIPISTE.ULK', + 'PERCENTRANK.EXC': 'PROSENTTIJÄRJESTYS.ULK', + KURT: 'KURT', + MODE: 'MOODI', + 'MODE.SNGL': 'MOODI.YKSI', + TRIMMEAN: 'KESKIARVO.TASATTU', QUARTILE: 'NELJÄNNES', 'QUARTILE.INC': 'NELJÄNNES.SIS', 'QUARTILE.EXC': 'NELJÄNNES.ULK', diff --git a/hyperformula/src/i18n/languages/frFR.ts b/hyperformula/src/i18n/languages/frFR.ts index f7b9be45f7..3f65d80f09 100644 --- a/hyperformula/src/i18n/languages/frFR.ts +++ b/hyperformula/src/i18n/languages/frFR.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'CENTILE', 'PERCENTILE.INC': 'CENTILE.INCLURE', 'PERCENTILE.EXC': 'CENTILE.EXCLURE', + 'PERCENTRANK.EXC': 'RANG.POURCENTAGE.EXCLURE', + KURT: 'KURTOSIS', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SIMPLE', + TRIMMEAN: 'MOYENNE.REDUITE', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INCLURE', 'QUARTILE.EXC': 'QUARTILE.EXCLURE', diff --git a/hyperformula/src/i18n/languages/huHU.ts b/hyperformula/src/i18n/languages/huHU.ts index e40219553a..6491e8ba60 100644 --- a/hyperformula/src/i18n/languages/huHU.ts +++ b/hyperformula/src/i18n/languages/huHU.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILIS', 'PERCENTILE.INC': 'PERCENTILIS.TARTALMAZ', 'PERCENTILE.EXC': 'PERCENTILIS.KIZÁR', + 'PERCENTRANK.EXC': 'SZÁZALÉKRANG.KIZÁR', + KURT: 'CSÚCSOSSÁG', + MODE: 'MÓDUSZ', + 'MODE.SNGL': 'MÓDUSZ.EGY', + TRIMMEAN: 'RÉSZÁTLAG', QUARTILE: 'KVARTILIS', 'QUARTILE.INC': 'KVARTILIS.TARTALMAZ', 'QUARTILE.EXC': 'KVARTILIS.KIZÁR', diff --git a/hyperformula/src/i18n/languages/idID.ts b/hyperformula/src/i18n/languages/idID.ts index 719cb11db6..1fb2f570a8 100644 --- a/hyperformula/src/i18n/languages/idID.ts +++ b/hyperformula/src/i18n/languages/idID.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'PERCENTILE.INC', 'PERCENTILE.EXC': 'PERCENTILE.EXC', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INC', 'QUARTILE.EXC': 'QUARTILE.EXC', diff --git a/hyperformula/src/i18n/languages/itIT.ts b/hyperformula/src/i18n/languages/itIT.ts index 1a494628f8..68a414bce8 100644 --- a/hyperformula/src/i18n/languages/itIT.ts +++ b/hyperformula/src/i18n/languages/itIT.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'INC.PERCENTILE', 'PERCENTILE.EXC': 'ESC.PERCENTILE', + 'PERCENTRANK.EXC': 'ESC.PERCENT.RANGO', + KURT: 'CURTOSI', + MODE: 'MODA', + 'MODE.SNGL': 'MODA.SNGL', + TRIMMEAN: 'MEDIA.TRONCATA', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'INC.QUARTILE', 'QUARTILE.EXC': 'ESC.QUARTILE', diff --git a/hyperformula/src/i18n/languages/nbNO.ts b/hyperformula/src/i18n/languages/nbNO.ts index ff15415a98..de2a75e5d0 100644 --- a/hyperformula/src/i18n/languages/nbNO.ts +++ b/hyperformula/src/i18n/languages/nbNO.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERSENTIL', 'PERCENTILE.INC': 'PERSENTIL.INK', 'PERCENTILE.EXC': 'PERSENTIL.EKS', + 'PERCENTRANK.EXC': 'PROSENTDEL.EKS', + KURT: 'KURT', + MODE: 'MODUS', + 'MODE.SNGL': 'MODUS.SNGL', + TRIMMEAN: 'TRIMMET.GJENNOMSNITT', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.INK', 'QUARTILE.EXC': 'KVARTIL.EKS', diff --git a/hyperformula/src/i18n/languages/nlNL.ts b/hyperformula/src/i18n/languages/nlNL.ts index c491e2f44b..1fbffe4dcc 100644 --- a/hyperformula/src/i18n/languages/nlNL.ts +++ b/hyperformula/src/i18n/languages/nlNL.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIEL', 'PERCENTILE.INC': 'PERCENTIEL.INC', 'PERCENTILE.EXC': 'PERCENTIEL.EXC', + 'PERCENTRANK.EXC': 'PROCENTRANG.EXC', + KURT: 'KURTOSIS', + MODE: 'MODUS', + 'MODE.SNGL': 'MODUS.ENKELV', + TRIMMEAN: 'GETRIMD.GEM', QUARTILE: 'KWARTIEL', 'QUARTILE.INC': 'KWARTIEL.INC', 'QUARTILE.EXC': 'KWARTIEL.EXC', diff --git a/hyperformula/src/i18n/languages/plPL.ts b/hyperformula/src/i18n/languages/plPL.ts index 6fa4f016b4..40ff68779a 100644 --- a/hyperformula/src/i18n/languages/plPL.ts +++ b/hyperformula/src/i18n/languages/plPL.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTYL', 'PERCENTILE.INC': 'PERCENTYL.PRZEDZ.ZAMK', 'PERCENTILE.EXC': 'PERCENTYL.PRZEDZ.OTW', + 'PERCENTRANK.EXC': 'PROC.POZ.PRZEDZ.OTW', + KURT: 'KURTOZA', + MODE: 'WYST.NAJCZĘŚCIEJ', + 'MODE.SNGL': 'WYST.NAJCZĘŚCIEJ.WART', + TRIMMEAN: 'ŚREDNIA.WEWN', QUARTILE: 'KWARTYL', 'QUARTILE.INC': 'KWARTYL.PRZEDZ.ZAMK', 'QUARTILE.EXC': 'KWARTYL.PRZEDZ.OTW', diff --git a/hyperformula/src/i18n/languages/ptPT.ts b/hyperformula/src/i18n/languages/ptPT.ts index 6c1e68c328..a990f3fba8 100644 --- a/hyperformula/src/i18n/languages/ptPT.ts +++ b/hyperformula/src/i18n/languages/ptPT.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INC', 'PERCENTILE.EXC': 'PERCENTIL.EXC', + 'PERCENTRANK.EXC': 'ORDEM.PERCENTUAL.EXC', + KURT: 'CURT', + MODE: 'MODO', + 'MODE.SNGL': 'MODO.SIMPLES', + TRIMMEAN: 'MÉDIA.INTERNA', QUARTILE: 'QUARTIL', 'QUARTILE.INC': 'QUARTIL.INC', 'QUARTILE.EXC': 'QUARTIL.EXC', diff --git a/hyperformula/src/i18n/languages/ruRU.ts b/hyperformula/src/i18n/languages/ruRU.ts index 6b35c1ce2f..7dc8d968d7 100644 --- a/hyperformula/src/i18n/languages/ruRU.ts +++ b/hyperformula/src/i18n/languages/ruRU.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'ПЕРСЕНТИЛЬ', 'PERCENTILE.INC': 'ПРОЦЕНТИЛЬ.ВКЛ', 'PERCENTILE.EXC': 'ПРОЦЕНТИЛЬ.ИСКЛ', + 'PERCENTRANK.EXC': 'ПРОЦЕНТРАНГ.ИСКЛ', + KURT: 'ЭКСЦЕСС', + MODE: 'МОДА', + 'MODE.SNGL': 'МОДА.ОДН', + TRIMMEAN: 'УРЕЗСРЕДНЕЕ', QUARTILE: 'КВАРТИЛЬ', 'QUARTILE.INC': 'КВАРТИЛЬ.ВКЛ', 'QUARTILE.EXC': 'КВАРТИЛЬ.ИСКЛ', diff --git a/hyperformula/src/i18n/languages/svSE.ts b/hyperformula/src/i18n/languages/svSE.ts index 5f87580087..4469185e66 100644 --- a/hyperformula/src/i18n/languages/svSE.ts +++ b/hyperformula/src/i18n/languages/svSE.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INK', 'PERCENTILE.EXC': 'PERCENTIL.EXK', + 'PERCENTRANK.EXC': 'PROCENTRANG.EXK', + KURT: 'TOPPIGHET', + MODE: 'TYPVÄRDE', + 'MODE.SNGL': 'TYPVÄRDE.ETT', + TRIMMEAN: 'TRIMMEDEL', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.INK', 'QUARTILE.EXC': 'KVARTIL.EXK', diff --git a/hyperformula/src/i18n/languages/trTR.ts b/hyperformula/src/i18n/languages/trTR.ts index 93c2176d37..46b659caf8 100644 --- a/hyperformula/src/i18n/languages/trTR.ts +++ b/hyperformula/src/i18n/languages/trTR.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'YÜZDEBİRLİK', 'PERCENTILE.INC': 'YÜZDEBİRLİK.DHL', 'PERCENTILE.EXC': 'YÜZDEBİRLİK.HRC', + 'PERCENTRANK.EXC': 'YÜZDERANK.HRC', + KURT: 'BASIKLIK', + MODE: 'ENÇOK_OLAN', + 'MODE.SNGL': 'ENÇOK_OLAN.TEK', + TRIMMEAN: 'KIRPORTALAMA', QUARTILE: 'DÖRTTEBİRLİK', 'QUARTILE.INC': 'DÖRTTEBİRLİK.DHL', 'QUARTILE.EXC': 'DÖRTTEBİRLİK.HRC', diff --git a/hyperformula/src/interpreter/functionMetadata/categories/statistical.ts b/hyperformula/src/interpreter/functionMetadata/categories/statistical.ts index 7a536e4cd2..7a02fd8826 100644 --- a/hyperformula/src/interpreter/functionMetadata/categories/statistical.ts +++ b/hyperformula/src/interpreter/functionMetadata/categories/statistical.ts @@ -297,6 +297,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=HYPGEOM.DIST(1, 4, 8, 20, FALSE())', '=HYPGEOM.DIST(1, 4, 8, 20, TRUE())'], }, + KURT: { + category: 'Statistical', + shortDescription: 'Returns the sample excess kurtosis of a set of numbers.', + parameters: [{name: 'number1', description: 'A number, cell reference, or range included in the calculation; at least four numbers with a non-zero spread are needed. Further numbers or ranges can be passed as additional arguments.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=KURT(1, 2, 3, 10)', '=KURT(A1:A10)'], + }, LARGE: { category: 'Statistical', shortDescription: 'Returns k-th largest value in a range.', @@ -402,6 +409,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=NORM.S.INV(0.5)'], }, + 'MODE.SNGL': { + category: 'Statistical', + shortDescription: 'Returns the most frequently occurring number; on a tie, the one that occurs first.', + parameters: [{name: 'number1', description: 'A number, cell reference, or range to search; text and boolean values in ranges are ignored. Further numbers or ranges can be passed as additional arguments.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=MODE.SNGL(1, 2, 2, 3)', '=MODE.SNGL(A1:A10)'], + }, 'PERCENTILE.EXC': { category: 'Statistical', shortDescription: 'Returns the k-th percentile of values in a range, exclusive of 0 and 1.', @@ -416,6 +430,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=PERCENTILE.INC(A1:A10, 0.9)'], }, + 'PERCENTRANK.EXC': { + category: 'Statistical', + shortDescription: 'Returns the rank of a value in a data set as a fraction between 0 and 1, exclusive of 0 and 1.', + parameters: [{name: 'data', description: 'The range of values to evaluate.'}, {name: 'x', description: 'The value to rank; it must lie between the smallest and the largest value in `data`. A value between two elements is interpolated.'}, {name: 'significance', description: 'Optional. The number of significant digits the result is truncated to; defaults to 3.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=PERCENTRANK.EXC(A1:A10, 5)', '=PERCENTRANK.EXC(A1:A10, 5.5, 4)'], + }, PHI: { category: 'Statistical', shortDescription: 'Returns probability density of normal distribution.', @@ -563,6 +584,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=T.TEST(A1:A10, B1:B10, 2, 1)'], }, + TRIMMEAN: { + category: 'Statistical', + shortDescription: 'Returns the mean of the values left after trimming the given fraction of the smallest and largest ones.', + parameters: [{name: 'data', description: 'The range of values to average.'}, {name: 'percent', description: 'The fraction of values to exclude, at least 0 and less than 1; the excluded count is rounded down to an even number, split equally between both ends.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=TRIMMEAN(A1:A10, 0.2)'], + }, TDIST: { category: 'Statistical', shortDescription: 'Returns density of Student-t distribution, both-sided or right-tailed.', diff --git a/hyperformula/src/interpreter/plugin/PercentilePlugin.ts b/hyperformula/src/interpreter/plugin/PercentilePlugin.ts index a09cd2c5c9..b86468e380 100644 --- a/hyperformula/src/interpreter/plugin/PercentilePlugin.ts +++ b/hyperformula/src/interpreter/plugin/PercentilePlugin.ts @@ -81,6 +81,14 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy {argumentType: FunctionArgumentType.NUMBER, greaterThan: 0, lessThan: 1}, ], }, + 'PERCENTRANK.EXC': { + method: 'percentrankExc', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER}, + {argumentType: FunctionArgumentType.NUMBER, defaultValue: 3}, + ], + }, 'QUARTILE.INC': { method: 'quartile', parameters: [ @@ -144,6 +152,45 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy ) } + /** + * Corresponds to PERCENTRANK.EXC(array, x, [significance]). + * + * Returns the exclusive rank of x in the array, i.e. the inverse of PERCENTILE.EXC, truncated (not rounded) + * to the given number of significant digits. Values between two array elements are linearly interpolated. + * + * @param ast - procedure AST node + * @param state - interpreter state + * @returns rank in (0, 1), #N/A if x is outside of the array, or CellError on invalid input + */ + public percentrankExc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('PERCENTRANK.EXC'), + (range: SimpleRangeValue, x: number, significance: number) => { + const digits = Math.trunc(significance) + if (digits < 1) { + return new CellError(ErrorType.NUM, ErrorMessage.LessThanOne) + } + const vals = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner()) + if (vals instanceof CellError) { + return vals + } + vals.sort((a, b) => a - b) + const n = vals.length + if (n === 0 || x < vals[0] || x > vals[n - 1]) { + return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) + } + if (n === 1) { + return 1 + } + const upperIndex = vals.findIndex(val => val >= x) + const rank = vals[upperIndex] === x + ? upperIndex + 1 + : upperIndex + (x - vals[upperIndex - 1]) / (vals[upperIndex] - vals[upperIndex - 1]) + const scale = Math.pow(10, digits) + return Math.floor(rank / (n + 1) * scale + 1e-9) / scale + } + ) + } + /** * Corresponds to QUARTILE(array, quart) and QUARTILE.INC(array, quart). * diff --git a/hyperformula/src/interpreter/plugin/StatisticalAggregationPlugin.ts b/hyperformula/src/interpreter/plugin/StatisticalAggregationPlugin.ts index c363bc1351..28965f57d7 100644 --- a/hyperformula/src/interpreter/plugin/StatisticalAggregationPlugin.ts +++ b/hyperformula/src/interpreter/plugin/StatisticalAggregationPlugin.ts @@ -152,8 +152,30 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func ], repeatLastArgs: 1, }, + 'KURT': { + method: 'kurt', + parameters: [ + {argumentType: FunctionArgumentType.ANY}, + ], + repeatLastArgs: 1, + }, + 'MODE.SNGL': { + method: 'modesngl', + parameters: [ + {argumentType: FunctionArgumentType.ANY}, + ], + repeatLastArgs: 1, + }, + 'TRIMMEAN': { + method: 'trimmean', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER}, + ], + }, } public static aliases = { + MODE: 'MODE.SNGL', COVAR: 'COVARIANCE.P', FTEST: 'F.TEST', PEARSON: 'CORREL', @@ -532,6 +554,100 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func return coerced.reduce((a, b) => a + Math.pow((b - avg) / s, 3), 0) / n }) } + + /** + * Corresponds to KURT(Number1, Number2, ...). + * + * Returns the sample excess kurtosis of the given numbers. + * + * @param ast + * @param state + */ + public kurt(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('KURT'), + (...args: RawInterpreterValue[]) => { + const coerced = this.arithmeticHelper.coerceNumbersExactRanges(args) + if (coerced instanceof CellError) { + return coerced + } + const n = coerced.length + if (n < 4) { + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.FourValues) + } + const avg = mean(coerced) + const s = stdev(coerced, true) + if (s === 0 || !Number.isFinite(s)) { + return new CellError(ErrorType.DIV_BY_ZERO) + } + const fourthMoment = coerced.reduce((a, b) => a + Math.pow((b - avg) / s, 4), 0) + return fourthMoment * n * (n + 1) / ((n - 1) * (n - 2) * (n - 3)) - 3 * Math.pow(n - 1, 2) / ((n - 2) * (n - 3)) + }) + } + + /** + * Corresponds to MODE(Number1, Number2, ...) and MODE.SNGL(Number1, Number2, ...). + * + * Returns the most frequent number; on a tie, the one that occurs first. Text and boolean values are ignored. + * + * @param ast + * @param state + */ + public modesngl(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('MODE.SNGL'), + (...args: InterpreterValue[]) => { + const values: number[] = [] + for (const arg of args) { + const scalars: InternalScalarValue[] = arg instanceof SimpleRangeValue ? arg.valuesFromTopLeftCorner() : [arg] + const numbers = this.arithmeticHelper.manyToExactNumbers(scalars) + if (numbers instanceof CellError) { + return numbers + } + for (const number of numbers) { + values.push(number) + } + } + const occurrences = new Map() + values.forEach(value => occurrences.set(value, (occurrences.get(value) ?? 0) + 1)) + let mode: number | undefined + let modeOccurrences = 1 + for (const value of values) { + const count = occurrences.get(value) as number + if (count > modeOccurrences) { + mode = value + modeOccurrences = count + } + } + return mode ?? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) + }) + } + + /** + * Corresponds to TRIMMEAN(array, percent). + * + * Returns the mean of the values left after dropping floor(n * percent / 2) of the smallest and the same + * number of the largest values. + * + * @param ast + * @param state + */ + public trimmean(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('TRIMMEAN'), + (range: SimpleRangeValue, percent: number) => { + if (percent < 0 || percent >= 1) { + return new CellError(ErrorType.NUM, percent < 0 ? ErrorMessage.ValueSmall : ErrorMessage.ValueLarge) + } + const values = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner()) + if (values instanceof CellError) { + return values + } + if (values.length === 0) { + return new CellError(ErrorType.NUM, ErrorMessage.OneValue) + } + values.sort((a, b) => a - b) + const trimmedPerSide = Math.floor(values.length * percent / 2) + return mean(values.slice(trimmedPerSide, values.length - trimmedPerSide)) + }) + } } /** diff --git a/hyperformula/src/license/functionCapabilities.ts b/hyperformula/src/license/functionCapabilities.ts index a26733ec5f..82cd4cb3ac 100644 --- a/hyperformula/src/license/functionCapabilities.ts +++ b/hyperformula/src/license/functionCapabilities.ts @@ -89,13 +89,13 @@ const UNGROUPED_FUNCTIONS = [ 'IMABS', 'IMAGINARY', 'IMARGUMENT', 'IMCONJUGATE', 'IMCOS', 'IMCOSH', 'IMCOT', 'IMCSC', 'IMCSCH', 'IMDIV', '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', + 'ISREF', 'KURT', 'LCM', 'LOG10', 'LOGNORM.DIST', 'LOGNORM.INV', 'MAXA', 'MAXPOOL', 'MEDIANPOOL', 'MINA', 'MIRR', + 'MMULT', 'MODE.SNGL', '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', + 'PERCENTRANK.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', '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', + 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'TRIMMEAN', 'UNICODE', 'VARA', 'VARPA', 'WEIBULL.DIST', 'WORKDAY.INTL', 'Z.TEST', ]