From 48aba9b396f2ad4f963a4135ce44f1d374519801 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 8 Oct 2026 03:10:17 +0000 Subject: [PATCH 1/6] Add KURT, MODE.SNGL, MODE, PERCENTRANK.EXC and TRIMMEAN functions Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 + src/error-message.ts | 1 + src/i18n/languages/csCZ.ts | 5 + src/i18n/languages/daDK.ts | 5 + src/i18n/languages/deDE.ts | 5 + src/i18n/languages/enGB.ts | 5 + src/i18n/languages/esES.ts | 5 + src/i18n/languages/fiFI.ts | 5 + src/i18n/languages/frFR.ts | 5 + src/i18n/languages/huHU.ts | 5 + src/i18n/languages/idID.ts | 5 + src/i18n/languages/itIT.ts | 5 + src/i18n/languages/nbNO.ts | 5 + src/i18n/languages/nlNL.ts | 5 + src/i18n/languages/plPL.ts | 5 + src/i18n/languages/ptPT.ts | 5 + src/i18n/languages/ruRU.ts | 5 + src/i18n/languages/svSE.ts | 5 + src/i18n/languages/trTR.ts | 5 + .../categories/statistical.ts | 28 +++++ src/interpreter/plugin/PercentilePlugin.ts | 43 +++++++ .../plugin/StatisticalAggregationPlugin.ts | 114 ++++++++++++++++++ src/license/functionCapabilities.ts | 8 +- 23 files changed, 277 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97c660139..ffe4b885a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 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 statistical functions `KURT`, `MODE.SNGL` (with the `MODE` alias), `PERCENTRANK.EXC` and `TRIMMEAN`. + ### Changed - Changed the `VERSION` function to return only the HyperFormula version (e.g. `HyperFormula v3.4.0`), without the license key status. [#1728](https://github.com/handsontable/hyperformula/pull/1728) diff --git a/src/error-message.ts b/src/error-message.ts index d42040901c..68ce600991 100644 --- a/src/error-message.ts +++ b/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/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fea286083c..1e75c19e64 100644 --- a/src/i18n/languages/csCZ.ts +++ b/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/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 9264551249..ab545e845e 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'FRAKTIL', 'PERCENTILE.INC': 'FRAKTIL.MEDTAG', 'PERCENTILE.EXC': 'FRAKTIL.UDELAD', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.MEDTAG', 'QUARTILE.EXC': 'KVARTIL.UDELAD', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 4d3d39319b..498678bac6 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'QUANTIL', 'PERCENTILE.INC': 'QUANTIL.INKL', 'PERCENTILE.EXC': 'QUANTIL.EXKL', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INKL', 'QUARTILE.EXC': 'QUARTILE.EXKL', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732cd..01e9ba9ec9 100644 --- a/src/i18n/languages/enGB.ts +++ b/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/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 9c6b817b98..6df267cefb 100644 --- a/src/i18n/languages/esES.ts +++ b/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': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'CUARTIL', 'QUARTILE.INC': 'CUARTIL.INC', 'QUARTILE.EXC': 'CUARTIL.EXC', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 99ff05708f..37ed516fc5 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PROSENTTIPISTE', 'PERCENTILE.INC': 'PROSENTTIPISTE.SIS', 'PERCENTILE.EXC': 'PROSENTTIPISTE.ULK', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'NELJÄNNES', 'QUARTILE.INC': 'NELJÄNNES.SIS', 'QUARTILE.EXC': 'NELJÄNNES.ULK', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index f7b9be45f7..c20c785164 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'CENTILE', 'PERCENTILE.INC': 'CENTILE.INCLURE', 'PERCENTILE.EXC': 'CENTILE.EXCLURE', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INCLURE', 'QUARTILE.EXC': 'QUARTILE.EXCLURE', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e40219553a..7126d19641 100644 --- a/src/i18n/languages/huHU.ts +++ b/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': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'KVARTILIS', 'QUARTILE.INC': 'KVARTILIS.TARTALMAZ', 'QUARTILE.EXC': 'KVARTILIS.KIZÁR', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db6..1fb2f570a8 100644 --- a/src/i18n/languages/idID.ts +++ b/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/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1a494628f8..01e3771e7e 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'INC.PERCENTILE', 'PERCENTILE.EXC': 'ESC.PERCENTILE', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'INC.QUARTILE', 'QUARTILE.EXC': 'ESC.QUARTILE', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index ff15415a98..b590f188ce 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERSENTIL', 'PERCENTILE.INC': 'PERSENTIL.INK', 'PERCENTILE.EXC': 'PERSENTIL.EKS', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.INK', 'QUARTILE.EXC': 'KVARTIL.EKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index c491e2f44b..d471850bf2 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIEL', 'PERCENTILE.INC': 'PERCENTIEL.INC', 'PERCENTILE.EXC': 'PERCENTIEL.EXC', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'KWARTIEL', 'QUARTILE.INC': 'KWARTIEL.INC', 'QUARTILE.EXC': 'KWARTIEL.EXC', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 6fa4f016b4..5e92f30c81 100644 --- a/src/i18n/languages/plPL.ts +++ b/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': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'KWARTYL', 'QUARTILE.INC': 'KWARTYL.PRZEDZ.ZAMK', 'QUARTILE.EXC': 'KWARTYL.PRZEDZ.OTW', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 6c1e68c328..a9cce3586e 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.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/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6b35c1ce2f..20522de04c 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'ПЕРСЕНТИЛЬ', 'PERCENTILE.INC': 'ПРОЦЕНТИЛЬ.ВКЛ', 'PERCENTILE.EXC': 'ПРОЦЕНТИЛЬ.ИСКЛ', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'КВАРТИЛЬ', 'QUARTILE.INC': 'КВАРТИЛЬ.ВКЛ', 'QUARTILE.EXC': 'КВАРТИЛЬ.ИСКЛ', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f87580087..3ae1599ed2 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INK', 'PERCENTILE.EXC': 'PERCENTIL.EXK', + 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.INK', 'QUARTILE.EXC': 'KVARTIL.EXK', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 93c2176d37..3a120069a9 100644 --- a/src/i18n/languages/trTR.ts +++ b/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': 'PERCENTRANK.EXC', + KURT: 'KURT', + MODE: 'MODE', + 'MODE.SNGL': 'MODE.SNGL', + TRIMMEAN: 'TRIMMEAN', QUARTILE: 'DÖRTTEBİRLİK', 'QUARTILE.INC': 'DÖRTTEBİRLİK.DHL', 'QUARTILE.EXC': 'DÖRTTEBİRLİK.HRC', diff --git a/src/interpreter/functionMetadata/categories/statistical.ts b/src/interpreter/functionMetadata/categories/statistical.ts index 7a536e4cd2..7a02fd8826 100644 --- a/src/interpreter/functionMetadata/categories/statistical.ts +++ b/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/src/interpreter/plugin/PercentilePlugin.ts b/src/interpreter/plugin/PercentilePlugin.ts index a09cd2c5c9..2ea9dfb5d4 100644 --- a/src/interpreter/plugin/PercentilePlugin.ts +++ b/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.INTEGER, minValue: 1, defaultValue: 3}, + ], + }, 'QUARTILE.INC': { method: 'quartile', parameters: [ @@ -144,6 +152,41 @@ 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 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, Math.trunc(significance)) + return Math.floor(rank / (n + 1) * scale + 1e-9) / scale + } + ) + } + /** * Corresponds to QUARTILE(array, quart) and QUARTILE.INC(array, quart). * diff --git a/src/interpreter/plugin/StatisticalAggregationPlugin.ts b/src/interpreter/plugin/StatisticalAggregationPlugin.ts index c363bc1351..bf49e4090a 100644 --- a/src/interpreter/plugin/StatisticalAggregationPlugin.ts +++ b/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,98 @@ 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) { + 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 + } + values.push(...numbers) + } + 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/src/license/functionCapabilities.ts b/src/license/functionCapabilities.ts index a26733ec5f..82cd4cb3ac 100644 --- a/src/license/functionCapabilities.ts +++ b/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', ] From 88563ba5572cfce3d5999ed023770cf5d190ba7e Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 8 Oct 2026 03:15:16 +0000 Subject: [PATCH 2/6] docs: link the pull request in the changelog 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 ffe4b885a1..9a88f1fe30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 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 statistical functions `KURT`, `MODE.SNGL` (with the `MODE` alias), `PERCENTRANK.EXC` and `TRIMMEAN`. +- Added the statistical functions `KURT`, `MODE.SNGL` (with the `MODE` alias), `PERCENTRANK.EXC` and `TRIMMEAN`. [#1800](https://github.com/handsontable/hyperformula/pull/1800) ### Changed From a4ce53c044361a7ede67de2c91c50591ccabb9d1 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 12 Oct 2026 03:46:05 +0000 Subject: [PATCH 3/6] docs: place the changelog entry under Unreleased Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a88f1fe30..cba6ff32be 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 statistical functions `KURT`, `MODE.SNGL` (with the `MODE` alias), `PERCENTRANK.EXC` and `TRIMMEAN`. [#1800](https://github.com/handsontable/hyperformula/pull/1800) + ### Fixed - Fixed the `EDATE` function returning the 28th of February instead of the 29th when the shifted date falls in February of a leap year (e.g. `=EDATE(DATE(2028, 1, 31), 1)` now returns `2028-02-29` instead of `2028-02-28`). [#1781](https://github.com/handsontable/hyperformula/pull/1781) From 04f2c4e56ae52b2c6a4af1f1f9a44b2354f67fa3 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 12 Oct 2026 06:45:09 +0000 Subject: [PATCH 4/6] Fix review findings: KURT on overflowing variance, approximate text match changelog Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 - src/interpreter/plugin/StatisticalAggregationPlugin.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cba6ff32be..8fd6d85c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 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 statistical functions `KURT`, `MODE.SNGL` (with the `MODE` alias), `PERCENTRANK.EXC` and `TRIMMEAN`. [#1800](https://github.com/handsontable/hyperformula/pull/1800) ### Changed diff --git a/src/interpreter/plugin/StatisticalAggregationPlugin.ts b/src/interpreter/plugin/StatisticalAggregationPlugin.ts index bf49e4090a..ba72271d5e 100644 --- a/src/interpreter/plugin/StatisticalAggregationPlugin.ts +++ b/src/interpreter/plugin/StatisticalAggregationPlugin.ts @@ -576,7 +576,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func } const avg = mean(coerced) const s = stdev(coerced, true) - if (s === 0) { + 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) From a268ef0c954d02fddd0c457a6159a0d067af38b5 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 15 Oct 2026 01:07:43 +0000 Subject: [PATCH 5/6] Add the Excel names of KURT, MODE, MODE.SNGL, PERCENTRANK.EXC and TRIMMEAN to the language packs Co-Authored-By: Claude Sonnet 5.5 --- src/i18n/languages/daDK.ts | 10 +++++----- src/i18n/languages/deDE.ts | 8 ++++---- src/i18n/languages/esES.ts | 10 +++++----- src/i18n/languages/fiFI.ts | 8 ++++---- src/i18n/languages/frFR.ts | 8 ++++---- src/i18n/languages/huHU.ts | 10 +++++----- src/i18n/languages/itIT.ts | 10 +++++----- src/i18n/languages/nbNO.ts | 8 ++++---- src/i18n/languages/nlNL.ts | 10 +++++----- src/i18n/languages/plPL.ts | 10 +++++----- src/i18n/languages/ptPT.ts | 10 +++++----- src/i18n/languages/ruRU.ts | 10 +++++----- src/i18n/languages/svSE.ts | 10 +++++----- src/i18n/languages/trTR.ts | 10 +++++----- 14 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index ab545e845e..774f2768cb 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'FRAKTIL', 'PERCENTILE.INC': 'FRAKTIL.MEDTAG', 'PERCENTILE.EXC': 'FRAKTIL.UDELAD', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 498678bac6..f9236f546b 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'QUANTIL', 'PERCENTILE.INC': 'QUANTIL.INKL', 'PERCENTILE.EXC': 'QUANTIL.EXKL', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + 'PERCENTRANK.EXC': 'QUANTILSRANG.EXKL', KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + MODE: 'MODALWERT', + 'MODE.SNGL': 'MODUS.EINF', + TRIMMEAN: 'GESTUTZTMITTEL', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INKL', 'QUARTILE.EXC': 'QUARTILE.EXKL', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 6df267cefb..ff93dddb58 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -390,11 +390,11 @@ export 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', + '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/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 37ed516fc5..a791edac29 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PROSENTTIPISTE', 'PERCENTILE.INC': 'PROSENTTIPISTE.SIS', 'PERCENTILE.EXC': 'PROSENTTIPISTE.ULK', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + 'PERCENTRANK.EXC': 'PROSENTTIJÄRJESTYS.ULK', KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + 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/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index c20c785164..3f65d80f09 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'CENTILE', 'PERCENTILE.INC': 'CENTILE.INCLURE', 'PERCENTILE.EXC': 'CENTILE.EXCLURE', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', + 'PERCENTRANK.EXC': 'RANG.POURCENTAGE.EXCLURE', + KURT: 'KURTOSIS', MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + 'MODE.SNGL': 'MODE.SIMPLE', + TRIMMEAN: 'MOYENNE.REDUITE', QUARTILE: 'QUARTILE', 'QUARTILE.INC': 'QUARTILE.INCLURE', 'QUARTILE.EXC': 'QUARTILE.EXCLURE', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index 7126d19641..6491e8ba60 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILIS', 'PERCENTILE.INC': 'PERCENTILIS.TARTALMAZ', 'PERCENTILE.EXC': 'PERCENTILIS.KIZÁR', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 01e3771e7e..68a414bce8 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'INC.PERCENTILE', 'PERCENTILE.EXC': 'ESC.PERCENTILE', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index b590f188ce..de2a75e5d0 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERSENTIL', 'PERCENTILE.INC': 'PERSENTIL.INK', 'PERCENTILE.EXC': 'PERSENTIL.EKS', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', + 'PERCENTRANK.EXC': 'PROSENTDEL.EKS', KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + MODE: 'MODUS', + 'MODE.SNGL': 'MODUS.SNGL', + TRIMMEAN: 'TRIMMET.GJENNOMSNITT', QUARTILE: 'KVARTIL', 'QUARTILE.INC': 'KVARTIL.INK', 'QUARTILE.EXC': 'KVARTIL.EKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index d471850bf2..1fbffe4dcc 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIEL', 'PERCENTILE.INC': 'PERCENTIEL.INC', 'PERCENTILE.EXC': 'PERCENTIEL.EXC', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 5e92f30c81..40ff68779a 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTYL', 'PERCENTILE.INC': 'PERCENTYL.PRZEDZ.ZAMK', 'PERCENTILE.EXC': 'PERCENTYL.PRZEDZ.OTW', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index a9cce3586e..a990f3fba8 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -390,11 +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', + '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/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 20522de04c..7dc8d968d7 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'ПЕРСЕНТИЛЬ', 'PERCENTILE.INC': 'ПРОЦЕНТИЛЬ.ВКЛ', 'PERCENTILE.EXC': 'ПРОЦЕНТИЛЬ.ИСКЛ', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + 'PERCENTRANK.EXC': 'ПРОЦЕНТРАНГ.ИСКЛ', + KURT: 'ЭКСЦЕСС', + MODE: 'МОДА', + 'MODE.SNGL': 'МОДА.ОДН', + TRIMMEAN: 'УРЕЗСРЕДНЕЕ', QUARTILE: 'КВАРТИЛЬ', 'QUARTILE.INC': 'КВАРТИЛЬ.ВКЛ', 'QUARTILE.EXC': 'КВАРТИЛЬ.ИСКЛ', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 3ae1599ed2..4469185e66 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -390,11 +390,11 @@ const dictionary: RawTranslationPackage = { PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INK', 'PERCENTILE.EXC': 'PERCENTIL.EXK', - 'PERCENTRANK.EXC': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 3a120069a9..46b659caf8 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -390,11 +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': 'PERCENTRANK.EXC', - KURT: 'KURT', - MODE: 'MODE', - 'MODE.SNGL': 'MODE.SNGL', - TRIMMEAN: 'TRIMMEAN', + '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', From baa2c001f48c11d26e5b439d9ebc3b1cd398fccd Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 15 Oct 2026 02:06:47 +0000 Subject: [PATCH 6/6] Truncate the PERCENTRANK.EXC significance like Excel, read MODE.SNGL values without spread Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 - src/interpreter/plugin/PercentilePlugin.ts | 8 ++++++-- src/interpreter/plugin/StatisticalAggregationPlugin.ts | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd6d85c71..415e2e072a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 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) - ### Changed - Changed the `VERSION` function to return only the HyperFormula version (e.g. `HyperFormula v3.4.0`), without the license key status. [#1728](https://github.com/handsontable/hyperformula/pull/1728) diff --git a/src/interpreter/plugin/PercentilePlugin.ts b/src/interpreter/plugin/PercentilePlugin.ts index 2ea9dfb5d4..b86468e380 100644 --- a/src/interpreter/plugin/PercentilePlugin.ts +++ b/src/interpreter/plugin/PercentilePlugin.ts @@ -86,7 +86,7 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy parameters: [ {argumentType: FunctionArgumentType.RANGE}, {argumentType: FunctionArgumentType.NUMBER}, - {argumentType: FunctionArgumentType.INTEGER, minValue: 1, defaultValue: 3}, + {argumentType: FunctionArgumentType.NUMBER, defaultValue: 3}, ], }, 'QUARTILE.INC': { @@ -165,6 +165,10 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy 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 @@ -181,7 +185,7 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy const rank = vals[upperIndex] === x ? upperIndex + 1 : upperIndex + (x - vals[upperIndex - 1]) / (vals[upperIndex] - vals[upperIndex - 1]) - const scale = Math.pow(10, Math.trunc(significance)) + const scale = Math.pow(10, digits) return Math.floor(rank / (n + 1) * scale + 1e-9) / scale } ) diff --git a/src/interpreter/plugin/StatisticalAggregationPlugin.ts b/src/interpreter/plugin/StatisticalAggregationPlugin.ts index ba72271d5e..28965f57d7 100644 --- a/src/interpreter/plugin/StatisticalAggregationPlugin.ts +++ b/src/interpreter/plugin/StatisticalAggregationPlugin.ts @@ -602,7 +602,9 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func if (numbers instanceof CellError) { return numbers } - values.push(...numbers) + for (const number of numbers) { + values.push(number) + } } const occurrences = new Map() values.forEach(value => occurrences.set(value, (occurrences.get(value) ?? 0) + 1))