diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7ab58d2..5956b2136 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 new functions: PERCENTILE, PERCENTILE.INC, PERCENTILE.EXC, QUARTILE, QUARTILE.INC, QUARTILE.EXC. [#1650](https://github.com/handsontable/hyperformula/pull/1650) + ### Fixed - Fixed the IRR function returning `#NUM!` error when the initial investment significantly exceeds the sum of returns. [#1628](https://github.com/handsontable/hyperformula/issues/1628) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index dd9777523..148d212e2 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -435,9 +435,15 @@ Total number of functions: **{{ $page.functionsCount }}** | NORMSINV | Returns value of inverse normal distribution. | NORMSINV(P) | | PEARSON | Returns the correlation coefficient between two data sets. | PEARSON(Data1, Data2) | | PHI | Returns probability densitity of normal distribution. | PHI(X) | +| PERCENTILE | Returns the k-th percentile of values in a range, inclusive of 0 and 1. | PERCENTILE(Data, K) | +| PERCENTILE.EXC | Returns the k-th percentile of values in a range, exclusive of 0 and 1. | PERCENTILE.EXC(Data, K) | +| PERCENTILE.INC | Returns the k-th percentile of values in a range, inclusive of 0 and 1. | PERCENTILE.INC(Data, K) | | POISSON | Returns density of Poisson distribution. | POISSON(X, Mean, Mode) | | POISSON.DIST | Returns density of Poisson distribution. | POISSON.DIST(X, Mean, Mode) | | POISSONDIST | Returns density of Poisson distribution. | POISSONDIST(X, Mean, Mode) | +| QUARTILE | Returns the quartile of a data set, based on inclusive percentile values. | QUARTILE(Data, Quart) | +| QUARTILE.EXC | Returns the quartile of a data set, based on exclusive percentile values. | QUARTILE.EXC(Data, Quart) | +| QUARTILE.INC | Returns the quartile of a data set, based on inclusive percentile values. | QUARTILE.INC(Data, Quart) | | RSQ | Returns the squared correlation coefficient between two data sets. | RSQ(Data1, Data2) | | SKEW | Returns skeweness of a sample. | SKEW(Number1, Number2, ...NumberN) | | SKEW.P | Returns skeweness of a population. | SKEW.P(Number1, Number2, ...NumberN) | diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index d0ed619fb..2558cd161 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'LARGE', SMALL: 'SMALL', + PERCENTILE: 'PERCENTIL', + 'PERCENTILE.INC': 'PERCENTIL.INC', + 'PERCENTILE.EXC': 'PERCENTIL.EXC', + QUARTILE: 'QUARTIL', + 'QUARTILE.INC': 'QUARTIL.INC', + 'QUARTILE.EXC': 'QUARTIL.EXC', AVEDEV: 'PRŮMODCHYLKA', CONFIDENCE: 'CONFIDENCE', 'CONFIDENCE.NORM': 'CONFIDENCE.NORM', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index d8838ff19..87521d93c 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMAGTAN', LARGE: 'STØRSTE', SMALL: 'MINDSTE', + PERCENTILE: 'FRAKTIL', + 'PERCENTILE.INC': 'FRAKTIL.MEDTAG', + 'PERCENTILE.EXC': 'FRAKTIL.UDELAD', + QUARTILE: 'KVARTIL', + 'QUARTILE.INC': 'KVARTIL.MEDTAG', + 'QUARTILE.EXC': 'KVARTIL.UDELAD', AVEDEV: 'MAD', CONFIDENCE: 'KONFIDENSINTERVAL', 'CONFIDENCE.NORM': 'KONFIDENS.NORM', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index d05f7dcdd..a15af87c8 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMATAN', LARGE: 'KGRÖSSTE', SMALL: 'KKLEINSTE', + PERCENTILE: 'QUANTIL', + 'PERCENTILE.INC': 'QUANTIL.INKL', + 'PERCENTILE.EXC': 'QUANTIL.EXKL', + QUARTILE: 'QUARTILE', + 'QUARTILE.INC': 'QUARTILE.INKL', + 'QUARTILE.EXC': 'QUARTILE.EXKL', AVEDEV: 'MITTELABW', CONFIDENCE: 'KONFIDENZ', 'CONFIDENCE.NORM': 'KONFIDENZ.NORM', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 233a354da..2ef663c1a 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'LARGE', SMALL: 'SMALL', + PERCENTILE: 'PERCENTILE', + 'PERCENTILE.INC': 'PERCENTILE.INC', + 'PERCENTILE.EXC': 'PERCENTILE.EXC', + QUARTILE: 'QUARTILE', + 'QUARTILE.INC': 'QUARTILE.INC', + 'QUARTILE.EXC': 'QUARTILE.EXC', AVEDEV: 'AVEDEV', CONFIDENCE: 'CONFIDENCE', 'CONFIDENCE.NORM': 'CONFIDENCE.NORM', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 7593a79ed..261bad4de 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -368,6 +368,12 @@ export const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'K.ESIMO.MAYOR', SMALL: 'K.ESIMO.MENOR', + PERCENTILE: 'PERCENTIL', + 'PERCENTILE.INC': 'PERCENTIL.INC', + 'PERCENTILE.EXC': 'PERCENTIL.EXC', + QUARTILE: 'CUARTIL', + 'QUARTILE.INC': 'CUARTIL.INC', + 'QUARTILE.EXC': 'CUARTIL.EXC', AVEDEV: 'DESVPROM', CONFIDENCE: 'INTERVALO.CONFIANZA', 'CONFIDENCE.NORM': 'INTERVALO.CONFIANZA.NORM', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 25d54032a..f40144435 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'KOMPLEKSI.TAN', LARGE: 'SUURI', SMALL: 'PIENI', + PERCENTILE: 'PROSENTTIPISTE', + 'PERCENTILE.INC': 'PROSENTTIPISTE.SIS', + 'PERCENTILE.EXC': 'PROSENTTIPISTE.ULK', + QUARTILE: 'NELJÄNNES', + 'QUARTILE.INC': 'NELJÄNNES.SIS', + 'QUARTILE.EXC': 'NELJÄNNES.ULK', AVEDEV: 'KESKIPOIKKEAMA', CONFIDENCE: 'LUOTTAMUSVÄLI', 'CONFIDENCE.NORM': 'LUOTTAMUSVÄLI.NORM', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 7733d20b4..cb6094f2b 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'COMPLEXE.TAN', LARGE: 'GRANDE.VALEUR', SMALL: 'PETITE.VALEUR', + PERCENTILE: 'CENTILE', + 'PERCENTILE.INC': 'CENTILE.INCLURE', + 'PERCENTILE.EXC': 'CENTILE.EXCLURE', + QUARTILE: 'QUARTILE', + 'QUARTILE.INC': 'QUARTILE.INCLURE', + 'QUARTILE.EXC': 'QUARTILE.EXCLURE', AVEDEV: 'ECART.MOYEN', CONFIDENCE: 'INTERVALLE.CONFIANCE', 'CONFIDENCE.NORM': 'INTERVALLE.CONFIANCE.NORMAL', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index d1341fb21..5e96528e7 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'KÉPZ.TAN', LARGE: 'NAGY', SMALL: 'KICSI', + PERCENTILE: 'PERCENTILIS', + 'PERCENTILE.INC': 'PERCENTILIS.TARTALMAZ', + 'PERCENTILE.EXC': 'PERCENTILIS.KIZÁR', + QUARTILE: 'KVARTILIS', + 'QUARTILE.INC': 'KVARTILIS.TARTALMAZ', + 'QUARTILE.EXC': 'KVARTILIS.KIZÁR', AVEDEV: 'ÁTL.ELTÉRÉS', CONFIDENCE: 'MEGBÍZHATÓSÁG', 'CONFIDENCE.NORM': 'MEGBÍZHATÓSÁG.NORM', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 28f4ece50..287c83b08 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'COMP.TAN', LARGE: 'GRANDE', SMALL: 'PICCOLO', + PERCENTILE: 'PERCENTILE', + 'PERCENTILE.INC': 'INC.PERCENTILE', + 'PERCENTILE.EXC': 'ESC.PERCENTILE', + QUARTILE: 'QUARTILE', + 'QUARTILE.INC': 'INC.QUARTILE', + 'QUARTILE.EXC': 'ESC.QUARTILE', AVEDEV: 'MEDIA.DEV', CONFIDENCE: 'CONFIDENZA', 'CONFIDENCE.NORM': 'CONFIDENZA.NORM', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 61ec76ccf..2922a72f1 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'N.STØRST', SMALL: 'N.MINST', + PERCENTILE: 'PERSENTIL', + 'PERCENTILE.INC': 'PERSENTIL.INK', + 'PERCENTILE.EXC': 'PERSENTIL.EKS', + QUARTILE: 'KVARTIL', + 'QUARTILE.INC': 'KVARTIL.INK', + 'QUARTILE.EXC': 'KVARTIL.EKS', AVEDEV: 'GJENNOMSNITTSAVVIK', CONFIDENCE: 'KONFIDENS', 'CONFIDENCE.NORM': 'KONFIDENS.NORM', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 4de49b7a4..a8f0898c5 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'C.TAN', LARGE: 'GROOTSTE', SMALL: 'KLEINSTE', + PERCENTILE: 'PERCENTIEL', + 'PERCENTILE.INC': 'PERCENTIEL.INC', + 'PERCENTILE.EXC': 'PERCENTIEL.EXC', + QUARTILE: 'KWARTIEL', + 'QUARTILE.INC': 'KWARTIEL.INC', + 'QUARTILE.EXC': 'KWARTIEL.EXC', AVEDEV: 'GEM.DEVIATIE', CONFIDENCE: 'BETROUWBAARHEID', 'CONFIDENCE.NORM': 'VERTROUWELIJKHEID.NORM', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 7c0a08756..ea33722b7 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'TAN.LICZBY.ZESP', LARGE: 'MAX.K', SMALL: 'MIN.K', + PERCENTILE: 'PERCENTYL', + 'PERCENTILE.INC': 'PERCENTYL.PRZEDZ.ZAMK', + 'PERCENTILE.EXC': 'PERCENTYL.PRZEDZ.OTW', + QUARTILE: 'KWARTYL', + 'QUARTILE.INC': 'KWARTYL.PRZEDZ.ZAMK', + 'QUARTILE.EXC': 'KWARTYL.PRZEDZ.OTW', AVEDEV: 'ODCH.ŚREDNIE', CONFIDENCE: 'UFNOŚĆ', 'CONFIDENCE.NORM': 'UFNOŚĆ.NORM', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index cd3fc715d..cbb131612 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'MAIOR', SMALL: 'MENOR', + PERCENTILE: 'PERCENTIL', + 'PERCENTILE.INC': 'PERCENTIL.INC', + 'PERCENTILE.EXC': 'PERCENTIL.EXC', + QUARTILE: 'QUARTIL', + 'QUARTILE.INC': 'QUARTIL.INC', + 'QUARTILE.EXC': 'QUARTIL.EXC', AVEDEV: 'DESV.MÉDIO', CONFIDENCE: 'INT.CONFIANÇA', 'CONFIDENCE.NORM': 'INT.CONFIANÇA.NORM', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 66032d3cd..38473a33a 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'МНИМ.TAN', LARGE: 'НАИБОЛЬШИЙ', SMALL: 'НАИМЕНЬШИЙ', + PERCENTILE: 'ПЕРСЕНТИЛЬ', + 'PERCENTILE.INC': 'ПРОЦЕНТИЛЬ.ВКЛ', + 'PERCENTILE.EXC': 'ПРОЦЕНТИЛЬ.ИСКЛ', + QUARTILE: 'КВАРТИЛЬ', + 'QUARTILE.INC': 'КВАРТИЛЬ.ВКЛ', + 'QUARTILE.EXC': 'КВАРТИЛЬ.ИСКЛ', AVEDEV: 'СРОТКЛ', CONFIDENCE: 'ДОВЕРИТ', 'CONFIDENCE.NORM': 'ДОВЕРИТ.НОРМ', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index d4741d6fc..f5952b74d 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'STÖRSTA', SMALL: 'MINSTA', + PERCENTILE: 'PERCENTIL', + 'PERCENTILE.INC': 'PERCENTIL.INK', + 'PERCENTILE.EXC': 'PERCENTIL.EXK', + QUARTILE: 'KVARTIL', + 'QUARTILE.INC': 'KVARTIL.INK', + 'QUARTILE.EXC': 'KVARTIL.EXK', AVEDEV: 'MEDELAVV', CONFIDENCE: 'KONFIDENS', 'CONFIDENCE.NORM': 'KONFIDENS.NORM', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 38507c24b..00d3ab417 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -368,6 +368,12 @@ const dictionary: RawTranslationPackage = { IMTAN: 'SANTAN', LARGE: 'BÜYÜK', SMALL: 'KÜÇÜK', + PERCENTILE: 'YÜZDEBİRLİK', + 'PERCENTILE.INC': 'YÜZDEBİRLİK.DHL', + 'PERCENTILE.EXC': 'YÜZDEBİRLİK.HRC', + QUARTILE: 'DÖRTTEBİRLİK', + 'QUARTILE.INC': 'DÖRTTEBİRLİK.DHL', + 'QUARTILE.EXC': 'DÖRTTEBİRLİK.HRC', AVEDEV: 'ORTSAP', CONFIDENCE: 'GÜVENİRLİK', 'CONFIDENCE.NORM': 'GÜVENİLİRLİK.NORM', diff --git a/src/interpreter/plugin/PercentilePlugin.ts b/src/interpreter/plugin/PercentilePlugin.ts new file mode 100644 index 000000000..a09cd2c5c --- /dev/null +++ b/src/interpreter/plugin/PercentilePlugin.ts @@ -0,0 +1,225 @@ +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import {CellError, ErrorType} from '../../Cell' +import {ErrorMessage} from '../../error-message' +import {ProcedureAst} from '../../parser' +import {InterpreterState} from '../InterpreterState' +import {InterpreterValue} from '../InterpreterValue' +import {SimpleRangeValue} from '../../SimpleRangeValue' +import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' + +/** + * Computes the inclusive percentile using linear interpolation: rank = k * (n - 1). + * Assumes sortedVals is non-empty and k is in [0, 1]. + * + * @param sortedVals - pre-sorted array of numeric values (ascending) + * @param k - percentile fraction in [0, 1] + * @returns interpolated percentile value + */ +function percentileInclusive(sortedVals: number[], k: number): number { + const n = sortedVals.length + const rank = k * (n - 1) + const lowerIndex = Math.floor(rank) + const fraction = rank - lowerIndex + if (lowerIndex + 1 < n) { + return sortedVals[lowerIndex] + fraction * (sortedVals[lowerIndex + 1] - sortedVals[lowerIndex]) + } + return sortedVals[lowerIndex] +} + +/** + * Computes the exclusive percentile using linear interpolation: rank = k * (n + 1). + * Assumes sortedVals is non-empty and k is in (0, 1). + * Returns CellError if the resulting rank falls outside [1, n]. + * + * @param sortedVals - pre-sorted array of numeric values (ascending) + * @param k - percentile fraction in (0, 1) + * @returns interpolated percentile value, or CellError if rank is out of bounds + */ +function percentileExclusive(sortedVals: number[], k: number): number | CellError { + const n = sortedVals.length + const rank = k * (n + 1) + // Exclusive method requires rank in [1, n]; values outside mean k is too extreme for this dataset size + if (rank < 1) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) + } + if (rank > n) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + const lowerIndex = Math.floor(rank) + const fraction = rank - lowerIndex + if (lowerIndex < n) { + return sortedVals[lowerIndex - 1] + fraction * (sortedVals[lowerIndex] - sortedVals[lowerIndex - 1]) + } + return sortedVals[lowerIndex - 1] +} + +/** + * Interpreter plugin for percentile and quartile statistical functions. + * + * Implements inclusive (INC) and exclusive (EXC) interpolation variants. + * QUARTILE functions delegate to PERCENTILE by converting quart index to + * a percentile fraction (quart / 4) after truncating to integer. + */ +export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTypecheck { + + public static implementedFunctions: ImplementedFunctions = { + 'PERCENTILE.INC': { + method: 'percentile', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER, minValue: 0, maxValue: 1}, + ], + }, + 'PERCENTILE.EXC': { + method: 'percentileExc', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER, greaterThan: 0, lessThan: 1}, + ], + }, + 'QUARTILE.INC': { + method: 'quartile', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER}, + ], + }, + 'QUARTILE.EXC': { + method: 'quartileExc', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER}, + ], + }, + } + + public static aliases = { + PERCENTILE: 'PERCENTILE.INC', + QUARTILE: 'QUARTILE.INC', + } + + /** + * Corresponds to PERCENTILE(array, k) and PERCENTILE.INC(array, k). + * + * Returns the k-th percentile of values in a range using inclusive interpolation. + * + * @param ast - procedure AST node + * @param state - interpreter state + * @returns interpolated percentile value, or CellError on invalid input + */ + public percentile(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('PERCENTILE.INC'), + (range: SimpleRangeValue, k: number) => { + const vals = this.getSortedValues(range) + if (vals instanceof CellError) { + return vals + } + return percentileInclusive(vals, k) + } + ) + } + + /** + * Corresponds to PERCENTILE.EXC(array, k). + * + * Returns the k-th percentile of values in a range using exclusive interpolation. + * + * @param ast - procedure AST node + * @param state - interpreter state + * @returns interpolated percentile value, or CellError on invalid input + */ + public percentileExc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('PERCENTILE.EXC'), + (range: SimpleRangeValue, k: number) => { + const vals = this.getSortedValues(range) + if (vals instanceof CellError) { + return vals + } + return percentileExclusive(vals, k) + } + ) + } + + /** + * Corresponds to QUARTILE(array, quart) and QUARTILE.INC(array, quart). + * + * Returns the quartile of a data set using inclusive interpolation. + * quart is truncated to an integer and validated in [0, 4]. + * + * @param ast - procedure AST node + * @param state - interpreter state + * @returns quartile value, or CellError on invalid input + */ + public quartile(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('QUARTILE.INC'), + (range: SimpleRangeValue, quart: number) => { + quart = Math.trunc(quart) + if (quart < 0) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) + } + if (quart > 4) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + const vals = this.getSortedValues(range) + if (vals instanceof CellError) { + return vals + } + // Convert quartile index to percentile fraction: 0→0%, 1→25%, 2→50%, 3→75%, 4→100% + return percentileInclusive(vals, quart / 4) + } + ) + } + + /** + * Corresponds to QUARTILE.EXC(array, quart). + * + * Returns the quartile of a data set using exclusive interpolation. + * quart is truncated to an integer and validated in [1, 3]. + * + * @param ast - procedure AST node + * @param state - interpreter state + * @returns quartile value, or CellError on invalid input + */ + public quartileExc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('QUARTILE.EXC'), + (range: SimpleRangeValue, quart: number) => { + quart = Math.trunc(quart) + if (quart < 1) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) + } + if (quart > 3) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + const vals = this.getSortedValues(range) + if (vals instanceof CellError) { + return vals + } + // Convert quartile index to percentile fraction: 1→25%, 2→50%, 3→75% + return percentileExclusive(vals, quart / 4) + } + ) + } + + /** + * Extracts numeric values from a range, filters non-numbers, and returns them sorted. + * Returns CellError if the range contains an error, or if no numeric values exist. + * + * @param range - input range from the spreadsheet + * @returns sorted numeric values (ascending), or CellError + */ + private getSortedValues(range: SimpleRangeValue): number[] | CellError { + const vals = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner()) + if (vals instanceof CellError) { + return vals + } + if (vals.length === 0) { + return new CellError(ErrorType.NUM, ErrorMessage.OneValue) + } + vals.sort((a, b) => a - b) + return vals + } +} diff --git a/src/interpreter/plugin/index.ts b/src/interpreter/plugin/index.ts index 6b79690f0..7cce2e53a 100644 --- a/src/interpreter/plugin/index.ts +++ b/src/interpreter/plugin/index.ts @@ -28,6 +28,7 @@ export {MathConstantsPlugin} from './MathConstantsPlugin' export {MatrixPlugin} from './MatrixPlugin' export {MedianPlugin} from './MedianPlugin' export {ModuloPlugin} from './ModuloPlugin' +export {PercentilePlugin} from './PercentilePlugin' export {NumericAggregationPlugin} from './NumericAggregationPlugin' export {PowerPlugin} from './PowerPlugin' export {RadiansPlugin} from './RadiansPlugin'